数组的本质
任何数组的元素在内存中都是连续字节存放的,数组下标必须是整数或整数表达式,并且下标操作符返回的是一个元素的引用:
1 | int arr[10] = { 0 }; |
使用[]引用数组元素的时候,编译器必须把它转成同类型指针然后编译,因为C/C++数组本身不会保存下标值和元素对象之间的对应关系
1 | arr[3] = 1; // 转换为*(arr+3)=1; |
我们可以写一段代码验证:
1 | int a = 10; |
这种做法是合理的,p[0]等价于*(p+0)
1 | int arr[5] = { 1, 2, 3, 4, 5 }; |
这样编译器会自动转变成*(2+arr)和arr[2]是等价的
数组初始化
静态变量初始化:存储于静态内存的数组只能在程序开始执行之前初始化一次,由链接器完成
自动变量初始化:自动变量位于运行时堆栈中,每次所处的内存位置并不一定相同,编译器没有办法对这些位置进行初始化,所以自动变量在缺省的情况下是未初始化的,如果自动变量给出了初始值,每次进入所在作用域时,变量就会被一条隐式的赋值语句初始化,这条语句也是需要时间和空间的
因此,如果在程序的执行流每次进入该函数(或代码块)时,每次初始化是不值得的,那么就将它声明为static
关于下标越界
编译器对数组下标几乎无法执行静态检查,因为下标引用可以作用与任意的指针,而不仅仅是数组名。作用于指针的下标引用的有效性既依赖于该指针当时恰好指向什么内容,也依赖于下标的值,编译器要证实下标表达式的结果引用的元素和指针表达式所指向的元素属于同一个数组,这需要空间存储数组的位置和长度信息,程序运行时还需要时间来更新这些信息,以反映自动和动态分配的数组
二维数组
在C/C++中以行序优先来存储的
1 | int arr[3][4] = { 1, 2, 3, 4, |
创建了arr数组,可以将他看作一个一维数组,包含三个元素,只不过每个元素恰好是包含4个整型元素的数组
访问arr[1][2]时,通过arr这个int (*) [4]指针访问元素,编译器不需要知道行数3,但是必须要知道列数4
多维数组
任何维数的数组都可以看做是由比它少一维的数组组成的一维数组,int a[3][4][5]可以看作3个4行5列的二维数组组成新的一维数组
数组名
《C和指针》中提到,这个概念实际上以一种相当优雅的方式把一些完全不同的概念联系在一起
数组名的值是一个常量指针,它的类型是指向其元素类型的常量指针,如上述arr就是int* const arr,由于不能仅通过数组名(不用下标和迭代)遍历数组,所以两个数组不能直接赋值。需要注意的是数组名具有一些和指针完全不同的特征比如,数组有确定的元素个数,指针是一个标量值。编译器用数组名记住这些属性
只有数组名在表达式中使用时,编译器才会为它产生一个指针常量
这两种情况下数组名并不是指针:
数组名作为sizeof操作符,或单目操作符&的操作数时,sizeof返回整个数组的长度,对数组名取地址返回的是指向数组的指针
数组和指针之间的关系
1 | int a[10] => int*const a; |
数组传递
1 | void test(int arr[], int size) { |
1 | int arr[100] = { 0 }; |
数组名的值就是首元素的地址,自然传递的时候是值传递,函数内部创建一个该指针的拷贝
编译器会改写成这样void test(int* arr, int size)在32位系统下指针是4个字节,函数内部下标访问编译器就会自动转换成为*(arr+i)并且可以修改arr的值
如果想按值传递数组,可以封装成struct
对于多维数组,必须传入除了第一维以外所有维的长度。ElemType arr[m][n][o]...[x][y][z]
和指针类型由这样的关系:ElemType(*const arr)[n][o]...[x][y][z]
数组的动态创建
C++动态创建二维数组注意事项:
1 | char* p = new char[5][4]; // 这是错误的定义方法 |
