前两天读了《C陷阱与缺陷》和《C专家编程》 两本书。回过头来看,在读这两本书之前我对C的认识实在是处于似懂非懂之间,尽管也写过不少代码。《C陷阱和缺陷》是作者收集的C语言中常见的认识和应用错误。其中讲到程序兼容性有一章,尤为难得,因为一般书籍很少涉及该话题。至于《C专家编程》,我强烈推荐。一方面是因为作者对C的理解很深刻,另一方面则是由于作者的文字流畅自如,其中还引用了不少关于C的趣闻,读着感觉很好,一点都不枯燥无味。《C专家编程》对数组和指针的分析尤为深刻。至少我觉得读过之后应该不会再把两者混淆起来了。
指针是C的精髓。数组和指针的关系包括以下几点:
1. 声明和定义中的数组和指针不等价。两本书中都给出了如下的例子:
int array[ 10 ] = { 0 };
// File 2
extern int * array;
这里的 array 指针和 array 数组绝不是一个东西。这样写法会引起错误(如果别的地方没有给出array指针的定义)。
2. 在使用时,数组和指针被编译器转换为同样的代码(当然不能把数组名当作左值进行++等操作)。也就是说,a[b] 与 *(a + b) 是等价的。也因此,下面的代码是合法的(很怪异但是却有效,可以去骗菜鸟了):
array[ 0 ] = 0 ; // 数组的第一个元素置为0
1 [array] = 1 ; // 数组的第二个元素置为1
int * ptr = array;
(( int * )(( int )ptr + sizeof (array)))[ - 1 ] = 9 ; // 数组的最后一个元素置为9
也正是这样,作为实参的数组名其实是将数组的地址传递给了调用它的函数,即数组作为实参是传址调用。在一个函数中对实际是一个数组的参数进行 sizeof 求值,并不能求出这个数组的长度,即使形参声明为数组也不行(实际将返回指针的长度)。
3. 当然,在内部实现上数组和指针还是不同的。这个可以把数组名看做定值,因此访问数组某个变量只需要将首地址加上偏移量。而指针指向的区域的首地址必须先访问指针所在的地址才能获得,然后将这个地址中的内容(即其指向内存区域的首地址)加上偏移量才能求出某个元素的真实地址。
4. 涉及二维数组的情况更为复杂。《C专家编程》指出C并不存在二维数组,而只存在数组的数组。即某数组的每个元素都是一个数组。因此一个二级指针(指针的指针)是不能直接指向一个二维数组的(这个错我经常犯……),必须写成:
int * p1 = & a[ 0 ][ 0 ]; // 合法,指向二维数组第一行的第一个元素
int ( * p2)[ 3 ] = & a[ 0 ]; // 合法,指向二维数组的第一行。这里[3]中的3不能省略
int ** p3 = a; // 错误!二级指针不能指向二维数组!
差不多最重要的几点就是这样。诸位见笑。