13 / 80 · C++11 · 约 8 分钟
extern:声明共享实体,而非重复分配
extern 常用于声明在别处定义的变量,让多个翻译单元引用同一个实体。必须区分声明和定义、作用域与链接;带初始化器的 extern 变量声明通常就是定义,头文件中的重复定义也不会被包含保护消除。
声明告诉编译器如何使用
extern int counter; 通常声明一个已有或将由其他位置定义的变量,不在这里提供变量定义。编译当前源文件时,编译器知道它的类型并可生成访问代码;链接程序时,还需要找到符合规则的实际定义。extern 不会让链接器凭空生成一个缺失变量。
常用组织方式是在头文件写声明,在一个源文件写 int counter = 0;。源文件本身也包含该头文件,让编译器检查声明与定义类型一致。示例将两者放在一个独立程序中演示关系,但真正的跨文件工程应保持同样的单一定义结构。
看到初始化器要重新判断
命名空间作用域的 extern int counter = 0; 是定义,不是“更明确的声明”。把它放进普通头文件后被多个翻译单元包含,会形成多份非 inline 定义。头文件保护只阻止同一翻译单元重复包含,不能合并不同翻译单元的定义。
函数的普通声明本身通常就具有外部链接,不需要为了跨文件调用而机械地添加 extern。另一方面,链接描述名字是否指向同一实体,作用域描述名字在哪里能被查找;一个局部 extern 声明不会把那个名字的可见范围自动扩展到全文件。
const 与共享常量
普通非 volatile 的命名空间 const 变量默认常有内部链接,除非存在 extern、inline 或先前外部链接声明等情况。希望共享一个常量对象时,可以在头文件写 extern const int limit;,并在包含该声明的源文件写 const int limit = 10;。
C++17 的 inline 变量适合直接在头文件提供一个全程序共享实体的定义;constexpr 与是否需要对象身份也要分别考虑。公开可变全局状态会增加依赖和并发负担,所以 extern 是组织已有共享实体的工具,不是鼓励把所有状态都变成全局。
容易答错的地方
- extern 不是运行时动态查找,也不意味着变量定义必须出现在文本更靠前的位置。
- 同名内部链接实体不能仅靠另一个源文件写 extern 就变成共享实体;应统一声明及定义的链接约定。
运行一个例子
最低标准 C++11 · 完整程序 · 下载 .cpp
#include <cassert>
#include <iostream>
extern int counter;
extern const int limit;
void increment() {
if (counter < limit) ++counter;
}
int counter = 0;
const int limit = 2;
int main() {
increment();
increment();
increment();
assert(counter == 2);
std::cout << counter << ' ' << limit << '\n';
}
在本地编译
g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread basics-extern.cpp -o example && ./example预期结果
2 2
CHECK YOUR UNDERSTANDING
合上答案,试着解释。
一个头文件写 int total = 0;,被两个源文件包含导致重复定义。请给出 C++11 下的声明、定义放置方案。
查看参考答案
头文件保留 extern int total;,删除头文件里的初始化器和变量定义。在唯一一个源文件中包含该头文件,并写 int total = 0;。其他源文件只包含头文件。这样各调用点共享同一个具有外部链接的对象,且不会因多个翻译单元各自产生定义而违反 ODR。
继续查证
标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。