翻译单元Translation Unit, TU)是源文件经过预处理后形成的单个编译输入;在 C/C++ 中,编译器通常一次只完整编译一个翻译单元,并为它生成一个目标文件

核心问题

开发者看到的是多个 .c.cpp.h 文件;编译器前端真正处理的不是单个物理源文件,而是“源文件加上所有被 #include 展开的头文件,再处理宏和条件编译后的结果”。

例如:

main.cpp
  #include "math_util.h"
  #include <vector>
 
预处理后 -> 一个很大的 main.cpp translation unit
编译后   -> main.o

因此,.cpp 文件不是严格等于翻译单元;.cpp 经过预处理后的完整文本才是翻译单元。

核心对象

对象作用
primary source file命令行传给编译器的 .c / .cpp 文件。
header files#include 展开的声明、模板、内联函数和宏定义。
preprocessor处理 include、宏替换、条件编译,生成翻译单元文本。
declarations告诉当前翻译单元某个函数、类型或变量存在。
definitions提供实体的真实实现或存储。
目标文件翻译单元编译后的输出,交给链接器

核心机制

普通构建把每个翻译单元独立编译:

g++ -O2 -c a.cpp -o a.o
g++ -O2 -c b.cpp -o b.o
g++ a.o b.o -o app

编译 a.cpp 时,如果只看到 int f(int); 这个声明而看不到 f 的函数体,编译器只能按外部调用生成代码。它不能可靠地把 f 内联进 a.cpp,也不能知道 f 是否总是返回某个常量。函数体可能在 b.cpp、静态库或动态库中。

LTO(链接时优化) 正是要突破这个边界:编译每个翻译单元时保存中间表示(IR),最终链接时把多个翻译单元的 IR 合起来或按摘要联合分析。

工程用途

理解翻译单元有助于解释这些 C/C++ 工程现象:

  • 头文件改动会导致所有包含它的翻译单元重新编译。
  • 模板和 inline 函数常放头文件,因为每个使用点的翻译单元都需要看到定义。
  • 普通跨文件函数只在链接时解析,编译期通常只检查声明是否可用。
  • 编译耗时和增量构建粒度常以翻译单元为基本单位。

边界与常见坑

  • 翻译单元不是二进制模块。它是编译输入;二进制输出是目标文件
  • 头文件本身通常不单独成为翻译单元,除非被当作主输入编译。
  • 多个翻译单元可以包含同一个头文件定义;这要求语言规则允许,例如模板、inline 函数、constexpr 变量或合适的内部链接。
  • C++ 的单一定义规则要求跨翻译单元的定义保持一致。普通构建可能到链接或运行时才暴露问题;LTO 因为能看到更多 IR,可能更早诊断或更激进优化。
  • 宏和条件编译会让不同翻译单元看到不同版本的同一个头文件内容,造成 ABI 或行为不一致。

相关术语