C++ / a working model

13 / 80   ·   C++11   ·   约 8 分钟

extern:声明共享实体,而非重复分配

先记住这句话

extern 常用于声明在别处定义的变量,让多个翻译单元引用同一个实体。必须区分声明和定义、作用域与链接;带初始化器的 extern 变量声明通常就是定义,头文件中的重复定义也不会被包含保护消除。

本篇内容
  1. 声明告诉编译器如何使用
  2. 看到初始化器要重新判断
  3. const 与共享常量
  4. 运行示例
  5. 动手练习

声明告诉编译器如何使用

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。

继续查证

标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。

回到目录