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 声明该变量。

目录
相关文章
|
16天前
|
C++
C++(十一)对象数组
本文介绍了C++中对象数组的使用方法及其注意事项。通过示例展示了如何定义和初始化对象数组,并解释了栈对象数组与堆对象数组在初始化时的区别。重点强调了构造器设计时应考虑无参构造器的重要性,以及在需要进一步初始化的情况下采用二段式初始化策略的应用场景。
|
16天前
|
C++
C++(十八)Smart Pointer 智能指针简介
智能指针是C++中用于管理动态分配内存的一种机制,通过自动释放不再使用的内存来防止内存泄漏。`auto_ptr`是早期的一种实现,但已被`shared_ptr`和`weak_ptr`取代。这些智能指针基于RAII(Resource Acquisition Is Initialization)原则,即资源获取即初始化。RAII确保对象在其生命周期结束时自动释放资源。通过重载`*`和`->`运算符,可以方便地访问和操作智能指针所指向的对象。
|
16天前
|
C++
C++(九)this指针
`this`指针是系统在创建对象时默认生成的,用于指向当前对象,便于使用。其特性包括:指向当前对象,适用于所有成员函数但不适用于初始化列表;作为隐含参数传递,不影响对象大小;类型为`ClassName* const`,指向不可变。`this`的作用在于避免参数与成员变量重名,并支持多重串联调用。例如,在`Stu`类中,通过`this->name`和`this->age`明确区分局部变量与成员变量,同时支持链式调用如`s.growUp().growUp()`。
|
27天前
|
搜索推荐 C语言
指针与数组
指针与数组
49 9
|
28天前
|
存储 安全 C++
C++:指针引用普通变量适用场景
指针和引用都是C++提供的强大工具,它们在不同的场景下发挥着不可或缺的作用。了解两者的特点及适用场景,可以帮助开发者编写出更加高效、可读性更强的代码。在实际开发中,合理选择使用指针或引用是提高编程技巧的关键。
23 1
|
1月前
|
算法 C++
c++学习笔记04 数组
这篇文章是C++学习笔记4,主题是数组。
36 4
|
1月前
|
安全 NoSQL Redis
C++新特性-智能指针
C++新特性-智能指针
|
1月前
|
编译器 C++
virtual类的使用方法问题之在C++中获取对象的vptr(虚拟表指针)如何解决
virtual类的使用方法问题之在C++中获取对象的vptr(虚拟表指针)如何解决
|
1月前
|
算法 Java
双指针在数组遍历中的应用
文章深入探讨了双指针技术在数组遍历中的应用,通过实战例子详细解释了快慢指针和首尾指针的不同用法,并提供了解决LeetCode相关问题的Java代码实现。
|
22天前
|
C++ 索引
C++数组、vector求最大值最小值及其下标
C++数组、vector求最大值最小值及其下标
44 0