[C++基础] 数组、指针、内存篇

一、数组

2.1 int a[2][2]= { {1}, {2,3} },则 a[0][1] 的值是多少?

二维数组的初始化一般有两种方式:

  • 第一种方式是按行来执行,如int array\[2][3]= { {0,0,1}, {1,0,0} };
  • 而第二种方式是把数值写在一块,如int array\[2][3]= { 0,0,1,1,0,0 };

若只对部分元素进行初始化,数组中未赋值的元素自动为赋值为 0,所以 a[0][1] 的值是0。

2.2 a是数组,(int*)(&a+1) 表示什么意思?

表示 int 类型的数组指针,若 a 为数组 a[5],则(int*)(&a+1)为 a[5]。

示例程序如下:

#include <stdio.h> 

void main()
{
	int a[5]={1,2,3,4,5};
	int b[ 100];
	int *ptr=(int*)(&a+1); 
	printf("%d %d\n",*(a+1),*(ptr-1));  // 2 5
	printf("sizeof(b)=%d\n",sizeof(b)); // sizeof(b)=400
	printf("sizeof(&b)=%d\n",sizeof(&b)); // sizeof(&b)=4
}

&a 是数组指针,是一个指向 int (*)[5] 的指针,所以 &a+1 的地址是 &a 地址再加 5*sizeof(int),它的运算单位是 int(*)[5]。经过类型转换后,ptr 相当于 int *[5]。

ptr-1 的单位是 ptr 的类型,因此 ptr-1 的位置刚好是 a[4]。因为 ptr 与 (&a+1) 类型是不一样的,所以 ptr-1 只会减去 sizeof(int*)。

值得注意的是,a 和 &a 的地址是一样的,但意思不一样,a 是数组首地址,也就是 a[0] 的地址;&a 是 对象(数组)首地址;a+1 是数组下一元素的地址,即 a[1];而 &a+1 是下一个对象的地址, 即 a[5]。

2.3 不使用流程控制语句,如何打印出1 ~ 1000的整数?

采用构造函数与静态构造变量结合的方法实现。首先在类中定义一个静态成员变量,然后在构造函数里面打印该静态变量的值,并对静态变量进行自增操作,同时在主函数里面定义一个类数组,程序代码示例如下:

class print
{
public:
	static int a;

	print()
	{
		printf("%d\n",print::a);
		a++;
	}
};
int print::a = 1;

int main()
{
	print tt[100]; 
	
	return 0;
}

2.4 int id[sizeof(unsigned long)]; 这个对吗?为什么?

答案:正确。这个 sizeof 是编译时运算符,编译时就确定了 ,可以看成和机器有关的常量。

扩展: 以下代码能够编译通过吗,为什么?

const int size1 = 2;
char str1[size1];

int temp = 0;
const int size2 = temp;
char str2[size2];

str1 能通过编译,而 str2 定义出错,size2 非编译器期间常量,而数组定义要求长度必须为编译期常量。

二、指针

1 使用指针有哪些好处?

一般而言,使用指针有以下几个方面的好处:

(1)可以动态分配内存;

(2)进行多个相似变量的一般访问;

(3)为动态数据结构,尤其是树和链表,提供支持;

(4)遍历数组,如解析字符串;

(5)高效地按引用 “复制” 数组与结构,特别是作为函数参数的时候,可以按照引用传递函数参数,提高开发效率。

2 引用和指针的区别?

(1)引用只能在定义时被初始化一次,之后不能被改变,即引用具有“从一而终”的特性。而指针却是可变的;

(2)引用使用时不需要解引用(*),而指针需要解引用;

(3)引用不可以为空,而指针可以为空;

(4)对引用进行 sizeof 操作得到的是所指向的变量(对象)的大小,而对指针进行 sizeof 操作得到的是指针本身(所指向的变量或对象的地址)的大小;

(5)作为参数传递时,两者不同。引用传递参数是 “引用传递”,会通过一个间接寻址的方式操作到主调函数中的相关变量。指针传递参数本质上是值传递的方式,它所传递的是一个地址值。

3 指针和数组是否表示同一概念

从原理与定义上看,虽然指针与数组表示的是不同的概念,但指针却可以方便地访问数组或者模拟数组,两者存在着一种貌似等价的关系,但也存在着诸多不同之处, 主要表现在以下两个方面:

(1)修改方式不同。

例如,char a[] = “hello”,可以通过取下标的方式对其元素值进行修改。例如,a[0] = ‘X’是正确的,而对于char *p = “world”,此时 p 指向常量字符串,所以p[0] = ‘X’是不允许的, 编译会报错。

(2)所占字节数不同。

例如,char *p = “world”,p 为指针,则sizeof(p)得到的是一个指针变量的字节数,而不是 p 所指的内存大小。而 sizeof 数组,得到的是数组所占的内存大小。

4 复杂声明?

void * ( * (*fp1)(int))[10];

float (*(* fp2)(int,int,int))(int);

int (* (* fp3)())[10]();

分别表示什么意思?

  • fp1 是一个指针,指向一个函数,这个函数的参数为 int 型,函数的返回值是一个指针,这个指针指向一个数组,这个数组有 10 个元素,每个元素是一个 void* 型指针;
  • fp2 是一个指针,指向一个函数,这个函数的参数为 3 个 int 型,函数的返回值是一个指针,这个指针指向一个函数,这个函数的参数为 int 型,函数的返回值是 float 型;
  • fp3 是一个指针,指向一个函数,这个函数的参数为空,函数的返回值是一个指针,这个指针指向一个数组,这个数组有 10 个元素,每个元素是一个指针,指向一个函数,这个函数的参数为空,函数的返回值是 int 型。

5 const 指针?

const char *p1; // 常量指针 — 指向“常量”的指针
char * const p2; // 指针常量 — 指针类型的常量

说明上面两种描述的区别;

(1)p1 本质上是一个指向 const char 的指针,可以改变指向,但是指向的值是不能改变的;

(2)p2 本质上是一个 const char 指针类型的常量,不可以改变指向,但是指向的值可以改变。

三、内存

1 内存分配方式有几种(并加以描述)?

内存分配方式有三种:

(1)从静态存储区域分配。内存在程序编译的时候就已经分配好,这块内存在程序的整个运行期间都存在。例如全局变量,static 变量。

(2)在栈上分配。在执行函数时,函数内局部变量的存储单元都可以在栈上创建,函数执行结束时这些存储单元自动被释放。栈内存分配运算内置于处理器的指令集。

(3)从堆上分配,亦称动态内存分配。程序在运行的时候用malloc 或new 申请任意多少的内存,程序员自己负责在何时用free 或delete 释放内存。动态内存的生存期由程序员决定,使用非常灵活,但问题也最多。

栈是向下增长的,堆是向上增长的。程序示例如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int global=0; // 静态存储区
char *pl; // 静态存储区

int main()
{
	int a; // 栈
	char s[]="abcdefg"; // 栈
	char *p2; // 栈
	char *p3="123456789"; // p3在栈上,"123456789"在常量区
	static int c=0; // 静态存储区
	pl=(char *)malloc(100); // 分配而来的100B的区域就在堆中 
	strcpy(p1,"123456789") // "123456789"放在常量区,编译器可能会将它与p3所指向的"123456789"优化成一个地方

	return 0;
}

2 栈空间和堆空间的最大值分别是多少?

在 Windows下,栈是向低地址扩展的数据结构,是一块连续的内存的区域。栈顶的地址和栈的最大容量是系统预先规定好的,在 Windows 下,栈的大小是 2MB;而Linux默认栈空间大小为8MB,可以通过命令 ulimit -s 来设置。

堆是向高地址扩展的数据结构,是不连续的内存区域。 这是由于系统是用链表来存储空闲内存地址的,自然是不连续的,而链表的遍历方向是由低地址向高地址的,而堆的大小受限于计算机系统中有效的虚拟内存。

Linux 虚拟地址空间内核占 1 GB,留给用户进程 3GB,Windows 是各占2GB,用户空间也是用户进程最大的堆申请数量了。但考虑到程序本身大小,动态库等因素,实际的堆申请数量是达不到最大值的,Linux小于3GB,Windows小于2GB。

3 堆和栈的区别?

堆栈空间分配

栈由操作系统自动分配释放 ,存放函数的参数值,局部变量的值等。其分配方式类似于数据结构中的栈。

堆一般由程序员分配释放, 若程序员不释放,程序结束时可能由操作系统回收。其分配方式类似于数据结构中的链表。

4 什么是内存泄漏?

所谓内存泄露是指由于疏忽或错误造成程序未能释放已经不再使用的内存的情况。内存泄漏并不是指内存在物理上的消失,而是应用程序分配某段内存后,因为设计错误,失去了对该段内存的控制,因而造成了内存的浪费。

例如,对指针进行重新赋值,程序代码如下:

char *memoryArea = malloc(lO); 
char *newArea = malloc(lO); 
memoryArea = newArea;

对 memoryArea 的赋值会导致 memoryArea 之前指向的内容丢失,最终造成内存泄露。

再看一个例子:

char *fun()
{
    return malloc(10);
}

void callfun()
{
    fun();
}

上面程序就因为未能对返回值进行处理,最终导致内存泄露。

怎样解决内存泄露?

在编码过程中养成良好的编程习惯,用 malloc 或 new 分配的内存都应当在适当的时机用 free 或 delete 释放,在对指针赋值前,要确保没有内存位置会变为孤立的,另外要正确处理使用动态分配的函数返回值。

5 什么是缓冲区溢出?

缓冲区是程序运行的时候机器内存中的一个连续块,它保存了给定类型的数据,随着动态分配变量会出现问题。缓冲区溢出是指当向缓冲区内填充数据位数超过了缓冲区自身的容量限制时,发生的溢出的数据覆盖在合法数据(数据、下一条指令的指针、函数返回地址等)上的情况。

程序示例如下:

#include <unistd.h> 

void Test()
{
	char buff[4]; 
	printf("Some input:"); 
	gets(buff); 
	puts(buff);
}	

该程序的 Test() 函数中使用了标准的 C 语言输入函数 gets(),由于它没有执行边界检查,最终会导致 Test() 函数存在缓冲区溢出安全漏洞。Test() 函数的缓冲区最多只能容纳 3 个字符和一个空字符,所以超过 4 个字符就会造成缓冲区溢出。

6 有关内存的思考题

思考题(一)

void GetMemory(char *p)
{
	p = (char *)malloc(100);
}

void Test(void)
{
	char *str = NULL;
	GetMemory(str);
	strcpy(str, "hello world");
	printf(str);
}

程序会崩溃。因为 GetMemory 并不能传递动态内存,Test 函数中的 str 一都是 NULL。

思考题(二)

char *GetMemory(void)
{
	char p[] = "hello world";
	return p;
}

void Test(void)
{
	char *str = NULL;
	str = GetMemory();
	printf(str);
}

可能是乱码。因为 GetMemory 返回的是指 “栈内存" 的指针,该指针的不是 NULL, 其原现的内容被清除,新内容不可知。

思考题(三)

void GetMemory2(char **p, int num)
{
	*p = (char *)malloc(num);
}

void Test(void)
{
	char *str = NULL;
	GetMemory(&str, 100);
	strcpy(str, "hello");
	printf(str);
}

能够输出 "hello",但会造成内存泄漏,因为没有通过 free 释放内存。

原文链接: https://www.cnblogs.com/linuxAndMcu/p/9865271.html

欢迎关注

微信关注下方公众号,第一时间获取干货硬货;公众号内回复【pdf】免费获取数百本计算机经典书籍

    [C++基础] 数组、指针、内存篇

原创文章受到原创版权保护。转载请注明出处:https://www.ccppcoding.com/archives/283931

非原创文章文中已经注明原地址,如有侵权,联系删除

关注公众号【高性能架构探索】,第一时间获取最新文章

转载文章受原作者版权保护。转载请注明原作者出处!

(0)
上一篇 2023年2月15日 上午7:30
下一篇 2023年2月15日 上午7:30

相关推荐