基于Windows10,Visual Studio 2019,x86,对C语言中的数组和数组名、二维数组、指针和const的理解和总结。
1.数组首先定义一个整型数组。
int arr[5] = { 1,2,3,4,5 };
对于数组需要注意的问题有:第一,数组 arr 在定义的时候有什么约束;第二,怎么求数组的长度;第三,数组元素在内存中是怎么存储的;第四,arr 代表什么意思;第五,数组作为函数参数时是如何传递的。
对于第一个问题,代码如下
#includeint main() { int n = 5; int arr[n] = { 1,2,3,4,5 }; return 0; }
运行之后会像下面一样报错
原因在于,C语言中数组的大小是在编译阶段就由编译系统分配,也就说在编译阶段就需要知道数组的大小,而 n 作为一个局部变量是在运行阶段才赋值的,所以会报错。因此,要么将 arr[n] 直接改成 arr[5],要么将 n 定义为 const int 类型。
对于第二个问题,比较常用的方法就是使用运算符 sizeof
int len = sizeof(arr) / sizeof(arr[0]);
这里需要额外注意的是 sizeof(arr),当 sizeof 的使用和 arr 的定义在同一个作用域时,sizeof(arr) 求得的是整个数组的字节数,当不在同一作用域时 sizeof(arr) 求得的是 arr 这个指针本身的字节数,看如下代码
之所以结果不同,是因为,当 arr 的定义和 sizeof 的使用在同一个作用域时, arr 是一个数组名,代表整个数组(这一点在第四个问题中进一步介绍),所以求得的是整个数组的大小。而当 arr 传递给函数时,arr 退化成一个指针(而这个指针的大小是由编译系统决定的,x86下的指针大小是32位,即4字节,x64下的指针大小是64位,8字节)。因此,sizeof 的使用应该和数组定义在同一个作用域,否则运行结果大概率是不对的。
第三个问题,数组中的元素在内存中的存储(基于Windows10,小端模式),代码如下
#includeint main() { int arr[5] = { 1,2,3,4,5 }; return 0; }
对其进行调试并查看内存
从上图可以看出,数组在内存中的存储空间是连续的,而且数组元素由低地址空间向高地址空间依次存储(单个元素按照小端模式存储,低地址存储低位数据,高地址存储高位数据)。
第四个问题,arr 代表什么含义。一般情况下,arr 都会被编译器当作一个指针来处理,该指针指向数组首元素的地址。但当出现下面两种情况的时候,arr 被认为是数组名,代表整个数组。
//第一种情况就是上面使用sizeof求数组元素个数时 sizeof(arr) //而且要求sizeof和arr的定义在同一个作用域 //第二种情况 &arr
第一种情况前面已经说明,来看一下第二种情况。
可以发现的是,arr 和 &arr 的值是一样的,arr + 1 和 arr 相差4个字节,&arr + 1 和 &arr 相差20个字节,怎么理解呢?前者的 arr 可以理解为一个指针(但实际上其类型并不是指针,而是 int [5]),该指针指向数组首元素的地址也就是指向 arr[0] 的地址,所以 arr + 1 增加了一个 int 的长度,也就是4字节。而后者中的 arr 可以理解为数组名(而且确实是数组名),代表整个数组,&arr 取的是整个数组的地址,&arr 的类型是 int (
∗
ast
∗)[5],是一个数组指针(关于指针和数组的关系在下一篇文章中介绍), 所以 &arr + 1 增加了一个 int [5] 的长度,也就是20字节,最后指向了一个包含五个未知数的数组。
第五个问题,数组作为函数参数时是如何传递的。在进行数组传递时采用的是传址,对于一维数组,把它的首地址和数组长度传过去。相对于传值方式主要有两方面优点。
- 节省空间。采用传值方式的话,需要额外开辟一个和原数组一样大小的数组,尤其当数组很大的时候十分浪费空间。
- 当我们需要修改传进来的数组时,传值方式无法完成,而传址却可以;当不允许修改时,只需要加一个 const,操作起来也很简单,如下
void Print(const int* arr, int len);二维数组
定义一个二维数组
int arr[2][3] = { {1,2,3}, {4,5,6} };
对二维数组需要注意的问题有:第一,二位数组 arr 在定义的时候有什么约束;第二,如何求得二维数组的行和列;第三,二维数组在内存中是如何存储的;第四,二维数组作为函数参数时如何传递。
第一个问题,和一维数组一样不能使用变量去定义数组的大小。二维数组特殊的一点是可以只指定列数,省略行数,如下
int arr[][3] = { {1,2,3}, {4,5,6} };
对于上面这个例子,似乎只指定行数或者列数,又或者都不指定都不会出现歧义,为了解释为什么只可以省略行数,而不能省略列数,另外定义一个数组(当数据不足时,会用0填充)
int brr[][3] = { 1,2,3,4,5 };
brr:
1 2 3
4 5 0
C语言中数组的定义是嵌套的,也就是说,二维数组本质上也是个一维数组,只不过它的元素是一维数组,三维数组也是个一维数组,它的元素是二维数组,以此类推。如果只指定行数如下
int brr[2][] = { 1,2,3,4,5 };
brr:
1 2 3
4 5 0
这样看起来似乎没有问题,但是如果想定义一个下面这样的二维数组
int brr[][4] = { 1,2,3,4,5 };
brr:
1 2 3 4
5 0 0 0
int brr[2][] = { 1,2,3,4,5 };
brr:???
计算机会自觉理解我们的想法,输出一个2
∗
ast
∗ 4 的矩阵吗?大概率不会。。。
对数组进行空间分配的时候,首先要知道数组元素的大小,其次才是元素的个数,数组元素的大小必须指定,而元素的个数,计算机可以自己推算出来。如果把任意维度的数组都看成二维数组,那么第一维代表的就是元素个数,其他维度组合起来加上类型代表的就是元素大小。这也是为什么对于所有维的数组来说,只能省略第一维,而不能省略其他维度。
第二个问题,求二维数组的行和列,如下
//行数 int rows = sizeof(arr) / sizeof(arr[0]); //列数 int columns = sizeof(arr[0]) / sizeof(arr[0][0]);
其中,sizeof(arr[0]) 正是二维数组一维化后元素的大小,arr[0] 的类型是 int [3],arr[0] + 1 的结果也会是增加4个字节。
第三个问题,二维数组在内存中是如何存储的。
从图中可以看出来,二维数组在内存中的存储方式是,从低地址到高地址,按行优先存储。
第四,二维数组作为函数参数时如何传递。相较于一维数组,二维数组及更高维数组在进行函数传递时除了第一个维度可以省略,其他维度都应该指明,如下
对于二维数组参数传递过程中,数组名 arr 将退化为一个 int (
∗
ast
∗) [3] 类型的指针。
上述关于数组的描述,内容其实已经涉及到指针。
int a = 10; int* p = &a;
指针本身也是一个变量,只不过这个变量比较特殊,它存储的是地址,而地址的大小是由编译系统决定的,那么指针变量的大小就是固定的,无论是几级指针(x86下是4字节,x64下是8字节)。由于指针本身也是一个变量,它也存在地址,所以存储指针地址的指针被定义为二级指针。指针的类型是 int
∗
ast
∗,二级指针的类型是int
∗
ast
∗
∗
ast
∗,但他们的大小是一样的
常见的几种指针如下
int *p;//指向 int 类型的指针 int **p;//指向 int ** 类型的二级指针 int *p[5];//指针数组,由于[]的优先级比*高,所以p是一个数组,包含五个元素,每个元素是一个 int * 类型的指针 int (*p)[5];//数组指针,()和[]的优先级一样,所以从左往右看,p是一个指针,该指针指向一个 int[5] 类型的数组
指针需要注意的地方如下
- 指针变量的加减运算并不是简单的加上或者减去一个整数,而是跟指针指向的数据类型有关
- 使用指针之前一定要初始化,否则就不能确定指针指向哪里(野指针),如果它指向的内存没有使用权限,程序就会崩溃,对于暂时没有指向的指针,可以赋值为NULL
- 当两个指针指向同一个数组中的元素时,两指针相减的结果就是两个指针之间相差的元素个数
- 除去 sizeof和& 两种情况,数组名一般都会被转换为一个指向数组首元素的指针
- 指针的类型不同时(虽然大小都相同),不能互相赋值,必须进行强制类型转换
const的位置不同,代表着不同的含义
int a = 10; const int *p = &a;//const修饰的是*p,而*p表示的是a,所以通过*p修改a的值是不允许的,但这里却可以修改p的值,也就是修改p存储的地址 int * const q = &a;// const修饰的是q,所以q存储的地址不能改变,但却可以修改*q的值,也就是可以通过*q修改a的值
由于const的修饰,在进行指针赋值的时候,要保证类型一致
const int b = 20; const int * p = &b;//等号右边的类型是const int *
另一个比较绕的地方是const修饰二级指针的时候
int a = 10; const int *p = &a; int ** q = &p;//这条语句是对的吗? int * const * q = &p;//这看起来好像对了? const int ** q = &p;//这才是最终的正确结果,因为右侧的类型是const int **总结
以上就是对数组和二维数组、指针和数组、指针和const、二级指针和const的一些分析和总结,内容算不上全面,后续会进行修改和补充。



