C++构建分编译(预处理、编译、汇编)和链接两阶段:预处理展开宏与头文件,编译生成汇编,汇编生成目标文件(.o),链接合并多个.o及库并解析符号地址;常见错误如undefined reference源于漏链接或依赖顺序不当,-c、-E、-S等选项可控制流程,依赖管理需构建系统支持。

编译阶段:预处理、编译、汇编三步走,g++ 默认连着干
你写的 .cpp 文件不是直接变机器码的,中间要过三道关。预处理展开 #include 和 #define,编译器把 C++ 语法翻译成汇编指令,最后汇编器把汇编转成目标文件(.o 或 .obj)。g++ 默认一步到位,但加 -c 就停在目标文件这步:g++ -c main.cpp 生成 main.o,不生成可执行文件。
常见错误现象:undefined reference to `main' 这类链接错误,其实常是因为漏了 -c 之后没继续链接,或者多个 .o 没一起喂给链接器。
- 预处理用
g++ -E main.cpp看展开后长啥样,特别适合查宏定义没生效的问题 -
g++ -S main.cpp只到汇编,生成main.s,能确认编译器是否真按你预期优化或内联 - 不同源文件各自
-c生成.o,是增量编译的基础,改一个文件不用全重编
链接阶段:把多个 .o 和库拼成一个 exe 或 a.out
目标文件里有符号(比如函数名、全局变量),但地址全是“占位符”。链接器负责填上真实内存地址,并解决跨文件引用。你写 std::cout,实际调用的是 libstdc++ 里的实现,链接时得把那部分代码也塞进来。
典型卡点:undefined reference to `foo()' 表示链接器在所有输入文件和库里都找不到 foo 的定义;multiple definition of `bar' 是同一个符号在多个 .o 里都定义了(比如头文件里写了非 inline 函数实现)。
立即学习“C++免费学习笔记(深入)”;
- 手动链接:把所有
.o一次性传给g++,比如g++ main.o utils.o -o program,g++此时只做链接,不重新编译 - 静态库是
.a文件,本质是一堆.o打包,链接时用-L/path/to/lib -lmylib;动态库是.so(Linux)或.dll(Windows),运行时才加载 - 链接顺序有影响:依赖别人的模块放前面,被依赖的放后面,否则可能报未定义——
g++ app.o -lm对,g++ -lm app.o在老版本 ld 上可能失败
g++ 一条命令背后的隐式步骤和开关控制
你以为 g++ main.cpp -o program 是“编译并链接”,其实它悄悄调了预处理器、编译器、汇编器、链接器四道程序。你可以用 -v 看它到底执行了哪些命令,路径、参数、默认库路径全打出来,调试构建问题时非常管用。
容易忽略的细节:C++ 标准库不是自动“免费”链接的。比如用了 std::thread 却没加 -pthread,Linux 下会链接失败;用了 std::filesystem(C++17),得显式加 -lstdc++fs。
-
-x c++强制把后缀非.cpp的文件当 C++ 处理,比如编译main.cc时g++有时会误判为 C -
-nostdlib完全不链标准启动代码和库,裸机或嵌入式开发才用,日常别碰 -
-Wl,--no-as-needed让链接器不跳过看起来“没用”的库,某些依赖间接调用的场景需要
为什么改了头文件却没重新编译?依赖关系谁在管
编译器自己不管依赖,g++ 生成的 .o 也不记录“我依赖哪些头文件”。所以你改了 utils.h,如果构建系统没告诉它 main.o 要重做,就会用旧的目标文件,导致行为不一致。
Makefile 或 CMake 的核心任务之一,就是维护这种依赖关系。C++ 编译器可以通过 -M 系列选项生成依赖规则:g++ -MM main.cpp 输出 main.o: main.cpp utils.h 这样的行,供 Make 读取。
-
-M输出所有依赖(含系统头),-MM只输出用户头文件,更实用 - CMake 默认开启依赖扫描,但如果你手写
add_executable时漏了某个.h文件,它不会自动感知——头文件不参与构建规则生成,除非你显式target_sources(... PRIVATE ...) - IDE(如 VS Code + CMake Tools)有时缓存旧的依赖图,改完头文件后清一下
build/目录再生成,比猜“为啥没生效”快得多











