再论数组
1.什么时候数组和指针相同
在实际应用中,数组和指针可以互换的情形比不可互换的情形要更为常见,让我们分别考虑“声明”和“使用”这两种情况;数组声明可以分成3种情况:
1.外部数组的声明(external array)
2.函数定义(定义是声明的特殊情况,它分配内存空间)
3.函数参数的声明
所有作为函数参数的数组总是可以通过编译器转换为指针,在使用数组(在语句或表达式中的引用)时,数组总是可以写成指针的形式,两者可以互换。
图1 指针和数组何时相同
数组和指针在编译处理时是不同的,在运行时的表示形式也是不一样的,并可能产生不同的代码。对编译器而言,一个数组就是一个地址,一个指针就是一个地址的地址
2.c标准中的数组规定
规则1:"表达式中的数组名"就是指针
对数组下标的引用总是可以写成“一个指向数组的起始地址的指针加上偏移量”;对数组的引用如a[i]在编译时总是被编译器改成*(a+i)的形式,在表达式中,指针和数组可以互换,因为在编译器里的最终形式都是指针。
编译器自动把下标值的步长调整到数组元素的大小。如果数组元素的大小是4字节,那么a[i]和a[i+1]在内存中的距离就是4。对起始地址执行加法之前,编译器会负责计算每次增加的步长。这就为什么指针总是有类型限制的,每个指针只能指向一个类型的原因所在:编译器需要知道对指针进行解引用操作时应该取几个字节,以及每个下标的步长应取几个字节。
规则2:c语言把数组下标作为指针的偏移量
规则3:“作为函数参数的数组名”等同于指针
在函数形参定义这个特殊情况下,编译器必须把数组形式改成指向数组第一个元素的指针形式,编译器只向函数传递数组的地址,而不是整个数组的拷贝。
例如:定义函数
在func函数的调用上,实参是数组和指针都是合法的void func(int *ptr){...} void func(int ptr[]){...} void func(int ptr[20]){...}
3.数组和指针的可交换性的总结
1)用a[i]这样的形式对数组进行访问总是被编译器解释为像*(a+i)这样的指针访问。
2)指针始终是指针。它绝不可以改成数组,你可以用下标形式访问指针,一般都是指针作为函数参数时,而且你知道实际传递给函数的是一个数组。3)把一个数组定义为函数的参数时,可以选择把它定义为数组,也可以定义为指针;不管是哪种,在函数内部获得的都是一个指针。4)在其他所有情况中,定义和声明必修匹配。如果定义了一个数组,在其它文件对它进行声明时也必须把它声明为数组,指针也是如此。
4.多维数组的内存布局
在C语言中数组的元素可以是另一个数组,有助于数组的分解,例如声明如下的三维数组:
int array[2][3][5]
图 2 整个数组的内存
图 3 sizeof[i]的内存空间
图4 sizeof[i][j]的内存空间
再如:图 5 sizeof[i][j][k]的内存空间
有人把二维数组看作是排列在一张表格中的一行行的一维数组,如下:
char ch[4][3];
图 6 假想的内存分布
实际的内存分布如下:
图 7 实际内存分布
ps:对于二维数组,像图6假想的内存分布有助于平时分析,但实际的内存分布不是这样子的
在C语言的多维数组中,最右边的下标最先变化,绝大数的语言都是采用这个约定且在堆栈内存朝低地址方向增长;
例如:
char b[2][2];