03 · C++ 编译模型与 CMake
目标:理解 C++ 从源文件到可执行文件的完整流程,以及 .h/.cpp 分离的原因,能独立用 CMake 构建多文件项目
一、编译流程总览
1 | 源文件 (.cpp) |
每个 .cpp 文件独立编译成一个 .o,链接器最后把所有 .o 合并。
二、第一步:预处理器
输入:原始 .cpp 源文件(含 #include、#define 等指令)
做了什么:
- 展开
#include:把头文件内容直接复制粘贴到#include的位置
1 | // 原始 main.cpp |
#include <iostream> 同理,展开后有几千行。
- 展开
#define:文本替换
1 |
|
- 处理条件编译:
#ifdef/#ifndef等,不满足条件的代码块直接删掉
输出:翻译单元——所有 # 指令处理完之后的纯 C++ 文本,没有任何 # 开头的指令。可以用 g++ -E main.cpp 查看。
三、第二步:编译器
输入:翻译单元(纯 C++ 文本)
做了什么:词法分析 → 语法分析(生成 AST)→ 语义分析(类型检查)→ 生成机器码
关键点:编译器只看当前翻译单元,遇到 add(1, 2) 的调用时:
1 | // main.cpp 展开后 |
编译器从声明知道参数类型和返回类型,可以生成正确的调用代码,但 add 的实现在哪里它不知道——在 .o 文件里留一个未解析符号,标记为 U(undefined)。
输出:.o 目标文件,包含:
- 机器码
- 符号表:已定义的符号(
T,有实现)和未解析的符号(U,地址待定)
1 | nm main.o |
四、第三步:链接器
输入:所有 .o 文件
1 | main.o → 机器码 + 未解析符号:add(U) |
做了什么:
- 合并所有
.o的机器码,分配最终地址 - 解析未解析符号:
main.o里add地址待定 → 在add.o里找到实现 → 填入真实地址
两种链接错误:
undefined reference to 'add':有声明和调用,但所有.o里都找不到定义multiple definition of 'add':多个.o里都有同名定义,不知道用哪个
输出:可执行文件,所有地址填好,可以直接运行
五、.h 和 .cpp 分离的原因
声明 vs 定义:
1 | int add(int a, int b); // 声明:描述函数签名,不含实现,可出现多次 |
分离的做法:
1 | // add.h:只放声明,被多个文件 #include,每次预处理展开一次,没有重复定义问题 |
数据流:
1 | add.h ──────────────────────────────────────┐ |
#include "add.h" vs #include <iostream>:
- 双引号:先在当前目录找,找不到再找系统路径
- 尖括号:只找系统路径(标准库、已安装的第三方库)
六、CMake
手动编译多文件项目繁琐,CMake 通过 CMakeLists.txt 描述项目结构,自动生成编译命令。
最小项目结构:
1 | my_project/ |
CMakeLists.txt:
1 | cmake_minimum_required(VERSION 3.10) |
add_executable(my_program main.cpp add.cpp) 等价于:
1 | g++ -c main.cpp -o main.o |
漏掉 add.cpp:链接器报 undefined reference to 'add'。
构建命令:
1 | mkdir build && cd build |
工业项目的组织方式: 不把所有文件列入 add_executable,而是用库来组织:
1 | add_library(my_lib add.cpp utils.cpp) |
大项目每个子目录有自己的 CMakeLists.txt,各自打包成库,顶层用 add_subdirectory 组合。
参考材料
- learncpp.com(编译流程):https://www.learncpp.com/cpp-tutorial/introduction-to-the-preprocessor/
- CMake Tutorial:https://cmake.org/cmake/help/latest/guide/tutorial/
