微观层面
在 C++ 中,每一个 .cpp 源文件都是独立编译的。这个过程通常分为四个步骤:
-
预处理 (Preprocessing)
-
做什么:处理所有以 # 开头的指令。
-
具体:把 #include "header.h" 的内容原封不动地复制粘贴到 .cpp 文件里;把 #define 定义的宏进行文本替换。
-
产物:一个巨大的、纯 C++ 代码的文本流(不再包含 # 指令)。
-
-
编译 (Compilation)
-
做什么 :把 C++ 代码翻译成汇编语言。
-
具体 :是否开启优化
-O2 / -O3;是否打开调试信息:-g;是否启用某些指令集:-march=native,-mavx2等;检查语法错误,将高级语言逻辑转换为 CPU 指令的助记符。 -
产物:汇编文件(.s)。
-
-
汇编 (Assembly)
-
做什么 :把汇编语言翻译成机器能读懂的二进制机器码。
-
产物 :目标文件 (Object File),在 Linux 下通常是 .o 文件,Windows 下是 .obj。
-
注意:此时,每个 .o 文件只包含自己那部分代码的机器码,如果代码里调用了别的文件的函数,这里只留了一个"坑"(占位符),并不知道具体地址。
-
-
链接 (Linking)
-
做什么:把成百上千个 .o 文件和各种库文件(.a, .so)打包在一起,生成最终的可执行文件或库。
-
具体:链接器会扫描所有 .o 文件,填上第3步留下的那些"坑"。比如 A 文件调用了 B 文件的函数,链接器负责把 A 中调用的地址指向 B 中函数的实际地址。
-
产物:最终的可执行程序(Executable)或 库(Library)。
-
静态链接 / 动态链接 :两者都是 先编译好库,区别在于:
-
静态链接:把库的代码拷贝一份进可执行文件里 → 可执行文件大,但独立性强。
-
动态链接 :运行时去加载
.so/.dll→ 文件小,但依赖运行环境要有对应的库。
-
-
宏观层面
对一个大型项目,往往会有:
-
若干静态库 / 动态库
-
若干可执行文件工具
-
单元测试、benchmark、demo
假如一个大一点的项目如下:
project/
src/
main.cpp
detector.cpp
utils.cpp
include/
detector.h
utils.h
third_party/
libsome_cv.a # 一个静态库
如果不构建系统的话就要写一堆命令:
# 编译每个 cpp 为 .o
g++ -Iinclude -c src/detector.cpp -o build/detector.o
g++ -Iinclude -c src/utils.cpp -o build/utils.o
g++ -Iinclude -c src/main.cpp -o build/main.o
# 链接,生成最终可执行文件
g++ build/main.o build/detector.o build/utils.o \
third_party/libsome_cv.a \
-o build/seatbelt_demo
再加上:
Debug/Release 两套配置
不同平台(Linux/Windows)
不同编译选项(开不开 O3、开不开 AVX)
单元测试、示例、工具程序...
这些东西的依赖关系一复杂,手写脚本就会很容易爆炸。
所以引入CMake只需要写工程描述就可以
来源于菜鸟教程的CMake解释
实际上的层面划分
源码 →(CMake)→ 构建系统(Ninja/Makefile/VS 工程)→ 调用编译器 → 可执行文件
-
CMake 干的事:
-
读取
CMakeLists.txt,理解"这个项目有哪些源文件、要生成什么可执行文件、需要哪些库、用什么编译选项" -
生成对应平台的"构建脚本",比如:
-
Linux/macOS:
Makefile或 Ninja 的build.ninja -
Windows:Visual Studio 的
.sln、.vcxproj
-
-
-
真正编译 C++ 的还是编译器 :
g++ / clang / cl.exe等CMake 只是在帮你把一长串编译命令自动拼出来,并且增量编译、跨平台都帮你处理好了。
流程其实是两步:
-
配置(configure):
-
读
CMakeLists.txt -
生成构建文件(Makefile / build.ninja / VS 工程等)
-
-
构建(build):
-
用上一步生成的构建文件
-
实际调用
g++/clang/cl去编译和链接
-
bash
cmake -S . -B build # 第一步:配置 -> 在 build/ 里生成构建脚本
cmake --build build # 第二步:构建 -> 真正编译
-
cmake:运行 CMake 程序。 -
-S .:Source ,源码目录在当前目录.(里面要有CMakeLists.txt)。 -
-B build:Binary / Build ,生成的构建文件放到build/目录里(推荐的 out-of-source build)。
"cmake --build build"只是 "cmake(程序) ------build(子命令) build(目录)"
还可以指定生成器:
因为 CMake 只是"中间人",得适配不同平台 / 工具链:
-
Unix Makefiles→ 生成Makefile,然后用make -
Ninja→ 生成build.ninja,然后用ninja -
Visual Studio 17 2022→ 生成.sln、.vcxproj -
还有 Xcode 等等
一般自己用的话,可以简单粗暴地记:
-
Linux/macOS:随便用默认,或者一直用 Ninja
bashcmake -S . -B build -G "Ninja"
Unix Makefiles(默认的 Make)
生成器选择建议:
-
如果你没啥特别需求 → 用 Ninja,爽一点
-
如果 Ninja 没装 / 老项目都用 Make → 用默认的也没问题
简单对比:
-
Ninja
-
编译速度通常更快(增量编译调度做得好)
-
输出信息更干净
-
配合 CMake 官方也推荐
-
-
Make (Unix Makefiles)
-
到哪基本都有
-
工具链、老项目兼容性好
-
但大项目下依赖计算、并行效率可能不如 Ninja
-
构建类型(Debug / Release)怎么选?谁好谁坏
这里分两类生成器看:
单配置生成器(Make / Ninja)
它们一次 build 只能是一个配置,所以要在 configure 阶段告诉 CMake:
常用的几种:
-
Debug-
几乎不开优化,保留完整调试信息
-
开发调试时用
-
-
Release-
开优化(
-O3等),可能不带调试信息 -
最终发布 / 跑性能时用
-
-
RelWithDebInfo(Release with Debug Info)-
开优化 + 保留调试信息
-
想兼顾"能调试 + 不至于太慢"时用
-
配置方式(Ninja/Make 常用):
bash
# Debug版本
cmake -S . -B build -G "Ninja" -DCMAKE_BUILD_TYPE=Debug
# Release版本
cmake -S . -B build-release -G "Ninja" -DCMAKE_BUILD_TYPE=Release
cmake --build build # 编译 Debug
cmake --build build-release # 编译 Release
可以给不同配置用不同 build 目录:build-debug, build-release,互不干扰
多配置生成器(VS / Xcode)
这类生成器一次可以包含多个配置(Debug/Release 都在工程里),所以
bash
cmake -S . -B build -G "Visual Studio 17 2022"
#配置时不写 CMAKE_BUILD_TYPE
cmake --build build --config Debug
cmake --build build --config Release
#构建时再选
最后 因为我是偏命令行编译C++ 就固定这样
bash
# 第一次 / 改配置时:
cmake -S . -B build -G "Ninja" -DCMAKE_BUILD_TYPE=Debug
# 之后每次改完代码只要:
cmake --build build -j$(nproc)
-j = jobs ,意思是:同时开多少个编译任务并行跑。
$(nproc) 是啥?这一段是 shell 语法 ,叫"命令替换":意思是:先执行括号里的命令,把输出结果 当作字符串塞回来= 先执行 nproc 这个命令,它会输出当前 CPU 有多少个逻辑核心
所以cmake --build build -j$(nproc)即为 在 build 目录里编译,并行任务数 = CPU 的核心数。
终极版总结三种角色
-
CMake
-
✅ 看
CMakeLists.txt -
✅ 根据你选的"生成器"(Ninja / Make / VS)生成对应的构建脚本
- 比如:
Makefile/build.ninja/.sln
- 比如:
-
❌ 不负责决定"什么时候重编什么文件"
-
-
构建系统(Make / Ninja / VS 等)
-
✅ 读取 CMake 生成的构建脚本
-
✅ 进行 增量编译判断:
-
哪些
.cpp改了 → 只编这些 -
哪些没改 → 直接复用上次的
.o
-
-
✅ 调用编译器去干活(编译、链接)
-
-
编译器(g++ / clang / cl)
-
✅ 负责把
.cpp→.o(+ 汇编这些细节) -
✅ 把一堆
.o+ 库 → 可执行文件 / 动态库
-
CMake 看 CMakeLists.txt,根据选的生成器 生成对应构建脚本;
构建系统 读这些脚本,做增量编译判断 ,然后调用 编译器 进行编译/链接生成可执行文件。
暂且算是把最基本的捋明白了 maybe 未完待续。。。