0%

C++ 中extern数组和指针

在一次使用extern声明全局变量的过程中,因为数组和指针的混用引发了错误。

我们知道,C++中使用extern来声明在其余(未使用include包含的)文件中的全局变量。现在问题是这样的:

在一个a.cpp中,有个全局变量

1
char a[] = "...";

在另一个b.cpp中,我想使用这个全局变量,由于固有的思想,指针和数组名通用,偷懒写成了如下形式:

1
extern char *a;

由此引发了一个segmentation fault错误。因此查阅了一下相关资料,发现指针和数组名是不能混用的。

指针和数组名的区别

数组名代表了存放该数组的那块内存,它是这块内存的首地址。这就说明了数组名是一个地址,而且,还是一个不可修改的常量,完整地说,就是一个地址常量。数组名跟枚举常量一样,都属于符号常量。数组名这个符号,就代表了那块内存的首地址。注意了!不是数组名这个符号的值是那块内存的首地址,而是数组名这个符号本身就代表了首地址这个地址值,它就是这个地址。这就是数组名属于符号常量的意义所在。由于数组名是一种符号常量,它是一个右值,而指针,作为变量,却是一个左值,一个右值永远都不是左值,那么,数组名永远都不会是指针!

关于这段话的理解,我觉得引入编译知识比较好理解,数组名是一个符号,和枚举符号一样,有其自身的值,数组名的值就是数组的首地址。在编译的过程中,这些符号常亮会被替换为地址符号。而指针是一个普通的变量,变量的值存放的是数组的地址。虽然数组名和指针都可以进行元素访问,但是其本质是有很大区别的!

char a[]中的a是常量,是一个地址,char *a中a是一个变量,一个可以存放地址的变量。

extern的问题

知道了上述的区别,再来看extern声明全局变量的内部实现:

被extern修饰的全局变量不被分配空间,而是在连接的时候到别的文件中通过查找索引定位该全局变量的地址。

1
extern char a[];

这是一个外部变量的声明,它声明了一个名为a的字符数组,编译器看到这个声明就知道不必为这个变量分配空间,这个.cpp文件中所有对数组a的引用都化为一个不包含类型的标号,具体地址的定位留给连接器完成。编译完成之后也得到一个中间文件,连接器遍历这个文件,发现有未经定位的标号,于是它搜索其他中间文件,试图寻找到一个匹配的空间地址,在此例中无疑连接器将成功地寻找到这个地址并将此中间文件中所有的这个标号替换为连接器所寻找到的地址,最终生成的可执行文件中,所有曾经的标号都应当已经被替换为地址。这是一个正常工作过程,连接出来的可执行文件至少在对于该数组的引用部分将工作得很好。

1
extern char * a;

这是一个外部变量的声明,它声明了一个名为a的字符指针,编译器看到这个声明就知道不必为这个指针变量分配空间,这个.cpp文件中所有对指针a的引用都化为一个不包含类型的标号,具体地址的定位留给连接器完成。编译完成之后仍然得到一个中间文件,连接器遍历这个文件,发现有未经定位的标号,于是它搜索其他中间文件,试图寻找到一个匹配的空间地址,经过一番搜索,找到了一个分配过空间的名为a的地方(也就是我们先定义的那个字符数组),连接器并不知道它们的类型,仅仅是发现它们的名字一样,就认为应该把extern声明的标号连接到数组a的首地址上,因此连接器把指针a对应的标号替换为数组a的首地址。这里问题就出现了:由于在这个文件中声明的a是一个指针变量而不是数组,连接器的行为实际上是把指针a自身的地址定位到了另一个.c文件中定义的数组首地址之上,而不是我们所希望的把数组的首地址赋予指针a(这很容易理解:指针变量也需要占用空间,如果说把数组的首地址赋给了指针a,那么指针a本身在哪里存放呢?)。这就是症结所在了。所以此例中指针a的内容实际上变成了数组a首地址开始的4字节表示的地址(如果在16位机上,就是2字节)。

通过上述分析,我们得到的最重要的结论是:使用extern修饰的变量在连接的时候只找寻同名的标号,不检查类型,所以才会导致编译通过,运行时出错。

参考资料