也说谭浩强的C教材 读谭师傅的书(1)
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))。

 

“C语言中的数组与指针”有5篇评论

  1. bluesea Says:

    这也是科普吗?我可以写很多了。



    这个大概算不上吧。不过你要是写我倒是愿意看:) — eng

  2. Says:

    而文件1中,我们只定义了一个叫做array的数组,因此在文件2中实际上我们找不到array,也没有为它分配空间,所以造成错误。
    =========
    这段描述不准确。
    记不得标准里有这个描述了,应该是依赖于compiler,懒得查了,惭愧一下。
    从你“Segmentation fault”的描述来看,大胆猜测一下,这个array的值可能是0,试图对0地址的内容修改自然“Segmentation fault”。
    实际上array是分配了空间的。只不过前面是给了个数组,其值都是0,后面将其值按指针来用,就成了
    array = 0;
    array[0] = 0;

    可以验证一下,去掉array[0] = 1;
    有两个文件,文件1中有如下定义:

    int array[10] = {4321};

    文件2中有如下声明和使用:

    extern int *array;
    int
    main(void)
    {
    printf(”%p\n”, array);
    }

    也许是10e1

    或者加上一行
    array = (int *)&array;
    array[0] = 1



        你说的对。我写得不够仔细。现在又加了一段,希望比较清楚了。

  3. bingfeng Says:

    unless you had not read it or else you’ll not make initial mistake.

  4. Says:

    虽然Expert C Programming里面谈到了这些,但是Peter并没有说的那么“直接”。其实已经挺直接了,不过篇幅不够大,而且那一章讲的是array和pointer Are Not the Same。很多人估计都没太注意。

    而这个问题从另外的角度看我认为更清楚,与其是不是array还是pointer没什么关系。其原因在于变量定义时的类型和使用时的类型不一致。而同一个符号名,所指代的地址空间是一个。
    Peter是做compiler的,他应该非常清楚,可能是像他举的例子一样,人人都不会int guava; extern float guava; 所以没有讲这样用compiler会如何处理,只是强调了array和pointer。而这么大的篇幅讲array和pointer,反而容易让有些人迷失。

    换成定义成int,使用extern double。呵呵实际上这也是我20年前第一次注意到这个问题的情景,是因为有人这样出了错。与其说array和pointer不同,不如说是extern(定义和声明)带来的不同。

    给个实例:
    f1.c:
    unsigned short d=0×3210;
    unsigned short e=0×7654;
    f2.c:
    extern unsigned long d;

    int main(void)
    {
    printf(”%lx\n”, d);

    在32位x86结果是什么?
    把上面的unsigned short和unsigned long替换成其他类型又如何

  5. 基本 Says:

    契的评论更有道理

    在编译器看来,文件1中的array和文件2中的array是两个不同的变量,各自有
    不同的scope。因为是两个不相干的变量,而文件2中的array值初设为NULL,故
    运行时“Segmentation fault”。

    虽然这两个文件中都用变量名array,且文件2写extern,并不保证他们是同一变量。
    C99规定“All declarations that refer to the same object or function
    shall have compatible type; otherwise, the behavior is undefined.”
    (6.2.7 Compatible type and composite type),而指针和数组则完全不
    compatible,正如你说的“从根本上来说是不同的类型”,只是错误不在于
    两个文件中使用相同的变量名array,错在文件2中的array其值为NULL,跟
    文件1中的array是两回事。如果把文件2改成
    extern double array;
    array = 1;
    就更清楚了,这时不会发生任何错误或混淆。

发表评论

CAPTCHA Image
*