从微观到宏观了解C++项目的编译

微观层面

在 C++ 中,每一个 .cpp 源文件都是独立编译的。这个过程通常分为四个步骤:

  1. 预处理 (Preprocessing)

    • 做什么:处理所有以 # 开头的指令。

    • 具体:把 #include "header.h" 的内容原封不动地复制粘贴到 .cpp 文件里;把 #define 定义的宏进行文本替换。

    • 产物:一个巨大的、纯 C++ 代码的文本流(不再包含 # 指令)。

  2. 编译 (Compilation)

    • 做什么 :把 C++ 代码翻译成汇编语言

    • 具体 :是否开启优化-O2 / -O3;是否打开调试信息:-g;是否启用某些指令集:-march=native, -mavx2 等;检查语法错误,将高级语言逻辑转换为 CPU 指令的助记符。

    • 产物:汇编文件(.s)。

  3. 汇编 (Assembly)

    • 做什么 :把汇编语言翻译成机器能读懂的二进制机器码

    • 产物目标文件 (Object File),在 Linux 下通常是 .o 文件,Windows 下是 .obj。

    • 注意:此时,每个 .o 文件只包含自己那部分代码的机器码,如果代码里调用了别的文件的函数,这里只留了一个"坑"(占位符),并不知道具体地址。

  4. 链接 (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 只是在帮你把一长串编译命令自动拼出来,并且增量编译、跨平台都帮你处理好了。

流程其实是两步:

  1. 配置(configure)

    • CMakeLists.txt

    • 生成构建文件(Makefile / build.ninja / VS 工程等)

  2. 构建(build)

    • 用上一步生成的构建文件

    • 实际调用 g++/clang/cl 去编译和链接

bash 复制代码
cmake -S . -B build      # 第一步:配置 -> 在 build/ 里生成构建脚本
cmake --build build      # 第二步:构建 -> 真正编译
  • cmake:运行 CMake 程序。

  • -S .Source ,源码目录在当前目录 .(里面要有 CMakeLists.txt)。

  • -B buildBinary / 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

    bash 复制代码
    cmake -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 + 库 → 可执行文件 / 动态库

CMakeCMakeLists.txt,根据选的生成器 生成对应构建脚本;
构建系统 读这些脚本,做增量编译判断 ,然后调用 编译器 进行编译/链接生成可执行文件。

暂且算是把最基本的捋明白了 maybe 未完待续。。。

相关推荐
另寻沧海2 小时前
C++ Lambda表达式的隐式转换陷阱
java·c++·算法
二川bro2 小时前
字符串特性解析:Python不可变性引发的错误
android·开发语言·python
好评1242 小时前
C++ 字符串:始于 char*,终于深拷贝
开发语言·c++·stl·字符串
小尧嵌入式2 小时前
QT软件开发知识点流程及记事本开发
服务器·开发语言·数据库·c++·qt
ByNotD0g2 小时前
Golang Green Tea GC 原理初探
java·开发语言·golang
qingyun9893 小时前
使用递归算法深度收集数据结构中的点位信息
开发语言·javascript·ecmascript
冷崖3 小时前
单例模式-创建型
c++·单例模式
努力学习的小廉3 小时前
【QT(三)】—— 信号和槽
开发语言·qt
盼哥PyAI实验室3 小时前
Python自定义HTTP客户端:12306抢票项目的网络请求管理
开发语言·python·http