Appearance
编译流程 / 静态与动态库 / 预处理
一、编译的 4 阶段(gcc/clang 内部仍相似)
.cpp --预处理--> .i --编译--> .s --汇编--> .o --链接--> 可执行/库| 阶段 | 做什么 | 命令 |
|---|---|---|
| 预处理 | 展开 #include/#define/#if、处理 #pragma | g++ -E |
| 编译 | 词法/语法/语义→IR→(优化)→汇编 | g++ -S (产出 .s 文本可读) |
| 汇编 | .s → 机器码 .o(含符号表/重定位表) | g++ -c |
| 链接 | 汇集 .o + 库,解析符号→可执行 | g++ |
关键知识点:
- 头文件:预处理把 #include 内容整体插入 → 每个 TU(.cpp 展开后)独立编译。
- 多个 .cpp include 同一头文件 → 每个 TU 各自有一份;inline 函数/模板因此靠“每 TU 定义 + weak symbol”保证不冲突。
- 模板/含实现头文件若到处 include → 编译时间与体积上升(= 增量编译慢的由来,module 想改善)。
二、编译单元(TU)与 ODR
- ODR(One Definition Rule):程序内每个非 inline 的实体只能有一次定义。inline 函数/变量、模板、类可多 TU 重复,但要一致。
- 由于每 TU 独立编译,跨 TU 的内联/常量传播做不了(这是 LTO 存在的理由,见 optimization)。
- .o 之间只在链接期看到符号,看不到函数体 → 链接顺序/重复符号问题在这里爆发。
三、静态库 vs 动态库
静态库 .a
- 链接时把用到的目标文件<copy 进可执行>, 一次性解决符号;产物自包含、易部署、启动快。
- 缺点:二进制大;库变更需重新链接;多进程各自一份代码副本,内存不共享浪费;热修复难。
动态库 .so(Linux)/ .dll
- 链接期只做符号引用检查/重定位记录,运行时由动态加载器装载映射;多进程共享同一份只读代码与 .rodata(省内存)。
- 优点:升级库不必重编可执行(改 ABI 除外)、节省内存、插件化。
- 缺点:运行时需能找到 so(LD_LIBRARY_PATH/rpath)、启动有加载开销、ABI 兼容风险(换 .so 但结构变了会崩)。
常见题:
ldd/readelf -d看依赖动态库;nm/objdump看符号。.a/.so的选择:一次性的自包含小工具 → 静态;多插件/库改造/共享内存 → 动态。
四、链接阶段两类符号处理
- 已定义(strong):找到唯一定义即绑定。
- 未定义引用(undefined reference):找不到 → 链接错误。
- 重复定义(multiple definition):同一 strong 符号多个 .o → 链接错误(inline 变量等例外用 weak)。
- weak symbol:可被多个定义,链接选 strong,缺失不报错(库特性、模板、弱插件探活使用)。
五、段(section)粗览
.text:代码;.rodata只读常量;.data已初始化全局/static;.bss未初始化(不占文件但占内存);.symtab/.rela符号/重定位。- 可执行加载地址、虚拟内存布局见 memory/layout。
六、为什么“实现放 .cpp、声明放 .h”是最好的纪律
- 头文件放实现会导致多 TU include 时重复代码/膨胀(除非 inline/模板)。
- 跨 TU 想显式实例化模板:
template class MyVec<int>;在某 .cpp + 头里extern template防重复(减少编译+链接体积)。
七、排查工具速记(命令)
g++ -E/-S/-c:单看各阶段。nm -C a.o(-C demangle)看符号;readelf -h/-s/-d;objdump -d反汇编;ldd动态依赖;file看 ELF 类型。