翻译单元(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 或行为不一致。
相关术语
- LTO(链接时优化)
- 目标文件
- 链接器
- 中间表示(IR)
- ABI