一个栗子
test.c文件
char p[10]="abcdefghij";
main.c文件
#include <stdio.h>
extern char *p;
int main() {
printf("%c\n",p[5]);
return 0;
}
大家觉得运行结果是什么呢?会是f吗
我们运行看看:
root@ubuntu:/home/wyf/clion/libevent# ./testlibevent
Segmentation fault
出现了段错误。
下面我们来分析分析原因。
数组并非指针
在分析这些问题之前,我们得先搞清楚这些内容,C语言中的对象必须有且只有一个定义,但是可以有多个声明。
| 定义 | 只能出现在一个地方 | int hz_array[10] |
|---|---|---|
| 声明 | 可以在多个地方出现多次 | extert int hz_array[]; |
c语言引入了可修改的左值这个术语。它表示左值允许出现在赋值语句的左边,这个奇怪的术语是为与数组名称区分,数组名也用于确定对象在内存的位置,也是左值,但其不能作为赋值的对象,因此,数组名是个左值但是不可修改。赋值符号必须用可修改的左值作为左边一侧的操作数。
出现在赋值符左边的符号有时被称为左值(由于它位于“左手边”或“表示地点”),出现在赋值符右边的符号有时则被称为右值(由于它位于“右手边”)。编译器为每个变量分配一个地址(左值)。
- 这个地址在编译时可知,而且该变量在运行时一直保存于这个地址。
- 相反,存储于变量中的值(它的右值)只有在运行时才可知。
如果需要用到变量中存储的值,编译器就发出指令从指定地址读入变量值并将它存于寄存器中。这里的关键之处在于每个符号的地址在编译时可知。所以,如果编译器需要--个地址(可能还需要加上偏移量)来执行某种操作,它就可以直接进行操作,并不需要加指令首先取得具体的地址。相反,对于指针,必须首先在运行时取得它的当前值,然后才能对它进行解除引用操作(作为以后进行查找的步骤之一)。图A展示了对数组下标的引用
这就是为什么extern char a[]与 extern char a[ 100]等价的原因。这两个声明都提示a是一个数组,也就是一个内存地址,数组内的字符可以从这个地址找到。编译器并不需要知道数组总共有多少长,因为它只产生偏离起始地址的偏移地址。从数组提取一个字符,只要简单地从符号表显示的a的地址加上下标,需要的字符就位于这个地址中。
相反,如果声明extern char *p,它将告诉编译器p是一个指针(在许多现代的机器里它是个四字节的对象),它指向的对象是一个字符。为了取得这个字符,必须得到地址.p 的内容,把它作为字符的地址并从这个地址中取得这个字符。指针的访问要灵活得多,但需要增加一次额外的提取,如图B所示。
char * p ="abcdefgh" ....p[3]
char a[]="abcdefgh" ....a[3]
在这两种情况下,都可以取得字符d',但两者的途径非常不一样。
当书写了extern char "p,然后用p[3]来引用其中的元素时,其实质是图A和图B访问方式的组合。首先,进行图3所示的间接引用。然后,如图A所示用下标作为偏移量进行直接访问。更为正式的说法是:编译器将会:
- 取得符号表中p的地址,提取存储于此处的指针
- 把下标所表示的偏移量与指针的值相加,产生一个地址。
- 访问上面这个地址,取得字符。
编译器已被告知p是一个指向字符的指针(相反,数组定义告诉编译器p是一个字符序列)。pli]表示“从p所指的地址开始,前进i步,每步都是一个字符(即每个元素的长度为一个字节)”。如果是其他类型的指针(如int或double等),其步长(每步的字节数)也各不相同。
既然把p声明为指针 那么不管p原先是定义为指针还是数组,都会按照上面所示的三个步骤进行操作,但是只有当p原来定义为指针时这个方法才是正确的。考虑一下p在这里被声明为extern char*p;而它原先的定义却是char p[ 10]:这种情形。当用p[li]这种形式提取这个声明的内容时,实际上得到的是一个字符。但按照上面的方法,编译器却把它当成是一个指针,把 ACSII字符解释为地址显然是牛头不对马嘴。如果此时程序当掉,你应该额手称庆。否则的话,它很可能会污染:程序地址空间的内容,并在将来出现莫名其妙的错误。