C++ 中 extern 数组和指针

简介: C++ 中 extern 数组和指针

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

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

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


char a[] = "...";


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


extern char *a;


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


指针和数组名的区别


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

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

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


extern 的问题


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

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

1


extern char a[];


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


extern char * a;


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

上述加粗部分的可以理解为,链接器认为 a 变量本身的内存位置是数组的首地址,但其实 a 的位置是其他位置,其内容才是数组首地址。

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


补充 extern 知识


另外补充一些 extern 知识

  • extern "C":按照 C 语言的标准编译代码,主要是符号不同。
  • extern int i = 0;:定义,extern 可以省略,i 可以在其他文件中使用。
  • extern int i;声明,i 在其他文件中定义。
  • int i:定义,分配了空间但未初始化。i 可以在其他文件中使用。
  • int i = 0:定义,分配了空间并初始化。
  • const int i =0:定义,const 对象是文件局部对象,因此 i 不可以在其他文件中使用。
  • extern const int i = 0:定义,i 是全局变量,可以在其他文件中使用。

为什么有 include 还需要 extern?

假如我们在头文件中定义一个全局变量,有多个文件同时 include 这个文件,我们知道 include 本质就是内容替换,因此就造成了该全局变量被重复定义。因此如果是多个文件链接在一起的情况,通常是在 cpp 文件中定义全局变量,而在另外一个 cpp 文件中使用时通过 extern 声明该变量。

目录
相关文章
|
13天前
使用指针访问数组元素
【10月更文挑战第30天】使用指针访问数组元素。
29 3
|
8天前
|
存储 编译器 Linux
【c++】类和对象(上)(类的定义格式、访问限定符、类域、类的实例化、对象的内存大小、this指针)
本文介绍了C++中的类和对象,包括类的概念、定义格式、访问限定符、类域、对象的创建及内存大小、以及this指针。通过示例代码详细解释了类的定义、成员函数和成员变量的作用,以及如何使用访问限定符控制成员的访问权限。此外,还讨论了对象的内存分配规则和this指针的使用场景,帮助读者深入理解面向对象编程的核心概念。
27 4
|
12天前
使用指针访问数组元素
【10月更文挑战第31天】使用指针访问数组元素。
25 2
|
24天前
|
存储 安全 编译器
在 C++中,引用和指针的区别
在C++中,引用和指针都是用于间接访问对象的工具,但它们有显著区别。引用是对象的别名,必须在定义时初始化且不可重新绑定;指针是一个变量,可以指向不同对象,也可为空。引用更安全,指针更灵活。
|
21天前
|
算法 索引
单链表题+数组题(快慢指针和左右指针)
单链表题+数组题(快慢指针和左右指针)
26 1
|
30天前
|
存储
如何使用指针数组来实现动态二维数组
指针数组可以用来实现动态二维数组。首先,定义一个指向指针的指针变量,并使用 `malloc` 为它分配内存,然后为每个子数组分配内存。通过这种方式,可以灵活地创建和管理不同大小的二维数组。
|
30天前
|
存储
如何通过指针数组来实现二维数组?
介绍了二维数组和指针数组的概念及其区别,详细讲解了如何使用指针数组模拟二维数组,包括定义与分配内存、访问和赋值元素、以及正确释放内存的步骤,适用于需要动态处理二维数据的场景。
|
30天前
|
存储 算法 C语言
C语言:什么是指针数组,它有什么用
指针数组是C语言中一种特殊的数据结构,每个元素都是一个指针。它用于存储多个内存地址,方便对多个变量或数组进行操作,常用于字符串处理、动态内存分配等场景。
|
1月前
魔法指针 之 二级指针 指针数组
魔法指针 之 二级指针 指针数组
19 1
|
1月前
|
存储
一篇文章了解区分指针数组,数组指针,函数指针,链表。
一篇文章了解区分指针数组,数组指针,函数指针,链表。
18 0