Mar 16

很多人认为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))。