很多人认为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))。
2009年3月16日 at 12:35 pm
这也是科普吗?我可以写很多了。
这个大概算不上吧。不过你要是写我倒是愿意看:) — eng
2009年3月17日 at 3:35 pm
而文件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
你说的对。我写得不够仔细。现在又加了一段,希望比较清楚了。
2009年3月17日 at 10:14 pm
unless you had not read it or else you’ll not make initial mistake.
2009年3月20日 at 11:48 am
虽然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替换成其他类型又如何
2009年4月22日 at 11:39 am
契的评论更有道理
在编译器看来,文件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;
就更清楚了,这时不会发生任何错误或混淆。