C++ 的“编译”不是一步完成,而是一条四阶段流水线 + 两种产物(静态/动态库) + 一个最终可执行文件。
把整条线串起来,就能一眼看出“入口文件”“子模块”“静态库/动态库”各自在哪一环出现、起什么作用。
一、四阶段流水线(核心概念)
-
预处理(Preprocessing)
只做文本替换:把#include的头文件内容整块拷贝进来,展开宏、去掉注释,生成.ii中间文件。
关键概念:翻译单元(translation unit)——一个.cpp经过预处理后的“完整源码”。 -
编译(Compilation)
对每个翻译单元做词法→语法→语义分析→优化,生成汇编文件.s。
此时才开始“懂 C++”:类、模板实例化、函数重载决议、内联、 constexpr 求值等。 -
汇编(Assembly)
把汇编文本转成目标文件.o/.obj:里面是机器码 + 未决议符号表(还缺别人实现)。 -
链接(Linking)
把所有.o和库(静态.a/.lib或动态.so/.dll)拼在一起,填上地址,生成- 最终可执行文件(ELF/PE/Mach-O),或
- 新的静态库(只是
.o的归档),或 - 新的动态库(带导出符号表,供后续动态链接)。
二、产物视角:静态库 vs 动态库 vs 可执行文件
| 产物 | 生成命令(GCC/Clang) | 内容 | 使用场景 |
|---|---|---|---|
静态库 libfoo.a | ar rcs libfoo.a a.o b.o | 一堆 .o 的压缩归档 | 链接阶段被完整拷贝进最终可执行文件;部署时不需要再带库文件。 |
动态库 libfoo.so / .dll | g++ -shared -fPIC a.o b.o -o libfoo.so | 机器码 + 导出符号表 + 重定位信息 | 可执行文件里只留“ stubs”;运行时才由操作系统加载器映射到进程地址空间;部署时必须一起发布。 |
可执行文件 app | g++ main.o -lfoo -o app | 入口符号 main + 已决议地址的机器码 | 双击/./app 直接运行。 |
三、入口到底在哪
-
对可执行文件
链接器找的符号是int main(int argc, char* argv[])(或main()/WinMain)。
谁提供这个符号,谁就是“入口源文件”——通常叫main.cpp/entry.cpp。 -
对静态/动态库
没有main!链接器不会把库当成“起点”。库只暴露一组 API 符号供别人调用。
因此库的“入口”概念是:头文件(foo.h)——告诉使用者有哪些类/函数可用。
四、子模块(组件)如何被“包含”
- 源码层面:用
#include "sub/foo.h"引入接口;实现文件sub/foo.cpp单独编译成sub/foo.o。 - 构建层面:
– 手写 Makefile / CMake / GN:把sub/foo.o列表一起送进链接器。
– 或者先把sub做成libsub.a/libsub.so,主程序链接时-lsub即可。 - 头文件只负责编译期检查;库文件负责链接期填符号;运行时动态库还要再加载一次。
五、一张图记住全流程
sub/foo.h ─┐
sub/foo.cpp ├→ 预处理 → 编译 → sub/foo.o ─┐
main.cpp ─┘ ├→ 链接 → app(可执行)
│
可选 ar → libsub.a
可选 g++ -shared → libsub.so
一句话总结
C++ 编译的核心是“翻译单元 → 目标文件 → 链接成库或可执行文件”;
只有可执行文件才找 main 当入口,库(静态/动态)只是符号仓库;
子模块通过“头文件 + 实现文件 → 目标文件 → 被链接”三步进入最终产物。