很多人认为C语言中的数组和常量指针是一回事。毕竟,如果我们有这样的代码:int a[10]; int * const p=a;,那么这两个变量a和p可以完全等价的使用。也就是说a[i]和p[i]都是访问的同一段内存空间,而且p和a一样都是只读的。那么,数组和常量指针在所有情况下都等价吗?在你回答“当然是”之前,请先看下面的代码:
有两个文件,文件1中有如下定义:
int array[10];
文件2中有如下声明和使用:
extern int *array;
array[0] = 1;
编译没有问题。运行起来,你猜怎么着?结果是“Segmentation fault”。为什么呢?原因就在于,数组和指针虽然有时可以混用,但从根本上来说是不同的类型。文件2相当于告诉编译器,说我有一个整数的指针叫做array,但它是在别的文件中定义的。而文件1中,我们只定义了一个叫做array的数组,所以造成错误。不过,为什么是“Segmentation fault”而不是其它呢?这就要看看数组和指针的区别到底在哪里。
在C程序中,每一个数组名在编译时,在Symbol Table里都对应着一个内存地址,这个地址就是该数组首元素的地址。相反,每一个指针p(无论是否常量)在Symbol Table里所对应的,是变量p的地址,而在我们为这个指针赋值后(int * const p=a;),这个变量p所在的内存空间就储存了另一个地址a,也就是真正数组首元素的地址。在访问数组时,如果我们说 a[1],则编译器会从Symbol Table里把a的地址找到,然后再加上a中一个元素的大小,得到一个新地址,再从该地址取得一个值。反之,如果我们说 p[1],那么编译器要先从Symbol Table拿到p的地址,取出其中存储的值,再把这个值加上p所指的类型大小,得到另一个地址,再到这个地址把值取出来。最后结果虽然一样,过程还是不同的。这个区别在下汇编代码中就可以看得很清楚。
我们先回头来看看刚才的“Segmentation fault”。当我们在文件1中定义数组array时,array在Symbol Table里是一个地址,这个地址下存放的是这个数组的首元素。而在文件2中,我们告诉编译器,我们定义的是一个指针array,也就是说,Symbol Table里的array代表的地址下所存放的,是另一个地址。所以当我们说“array[0] = 1”时,程序实际从array所代表的地址中取得了一个数,把它解释成另一个地址,再试图对这第二个地址中内容进行写操作。在我们这个例子中,array在文件1中没有初始化,所有的元素都被编译器设成了0。所以在文件2中,当我们把array当成指针时,其中存放的值就是0。而对地址0(也就是NULL)进行写操作当然会出现“Segmentation fault”。
最后,我们再来看看从编译器产生的汇编代码,数组和指针的区别就更加一目了然。首先创建一个文件叫test.c,内容如下:
#include <stdio.h>
int a[10];
int * const p=a;
int main()
{
a[0] = 1;
p[0] = 2;
return 0;
}
在Linux上经gcc -S编译后得到如下结果:
.file “test.c”
.globl p
.section .rodata
.align 4
.type p, @object
.size p, 4
p:
.long a
.text
.globl main
.type main, @function
main:
leal 4(%esp), %ecx
andl $-16, %esp
pushl -4(%ecx)
pushl %ebp
movl %esp, %ebp
pushl %ecx
movl $1, a
movl p, %eax
movl $2, (%eax)
movl $0, %eax
popl %ecx
popl %ebp
leal -4(%ecx), %esp
ret
.size main, .-main
.comm a,40,32
.ident “GCC: (Ubuntu 4.3.2-1ubuntu12) 4.3.2″
.section .note.GNU-stack,”",@progbits
我们可以清楚地看到,当赋值给a[0]时,只需要一个movl语句,即movl $1, a,而赋值给p[0]时,则需要先拿p的内容(movl p, %eax),再用间接寻址模式进行赋值(movl $2, (%eax))。