现代 C++ 实战(01):CMake 与现代构建
上一篇我们把环境跑通了。从这一篇开始,我们真正进入 C++ 工程的地基:CMake。本系列 39 个 demo 全部用 CMake 构建,其中 projects/fetch_content/ 是一个「集大成」示例——它用 FetchContent 自动拉取 gtest、json、fmt、spdlog 等 6 个第三方库,演示现代 C++ 项目最常见的依赖管理方式。
搞懂这篇,后面所有带外部库的 demo 你都不会慌。
这是「现代 C++ 实战」系列的第 1 篇。对应 demo:
ref/cpp_demo/projects/fetch_content/。建议先读 第 00 篇:环境搭建与项目导览。
一、为什么需要 CMake?
手写 Makefile 在小项目里够用,但现代 C++ 项目很快会遇到天花板:
| 痛点 | Makefile 的局限 | CMake 的做法 |
|---|---|---|
| 跨平台 | Windows / macOS / Linux 语法不同 | 一套 CMakeLists.txt,生成对应平台的构建文件 |
| 依赖管理 | 手动下载、编译、写 -I / -L |
FetchContent、find_package 等模块 |
| C++ 标准 | 编译器 flag 各写各的 | CMAKE_CXX_STANDARD 统一声明 |
| IDE 集成 | 难以生成索引数据库 | CMAKE_EXPORT_COMPILE_COMMANDS → clangd |
| 目标抽象 | 文件列表散落各处 | add_library / add_executable / target_link_libraries |
CMake 不是编译器,而是构建系统生成器:它读 CMakeLists.txt,输出 Makefile、Ninja 或 Visual Studio 工程,再由底层工具完成编译链接。
可以把它想成「建筑蓝图」:你画的是房间和管线(target 与依赖),至于用什么施工队(Make / Ninja / MSBuild),CMake 帮你对接。
二、最小 CMake 项目
一个能编译运行的 C++ 程序,最少需要三行 CMake:
1 | cmake_minimum_required(VERSION 3.14) |
| 指令 | 作用 |
|---|---|
cmake_minimum_required |
声明所需最低 CMake 版本,低于此版本直接报错 |
project |
定义项目名、版本,并初始化编译器检测 |
add_executable |
从源文件生成可执行目标 |
典型工作流:
1 | mkdir build && cd build |
本系列 demo 把上述步骤封装进了 ./build.sh,等价于:
1 | cd ref/cpp_demo/projects/fetch_content |
三、C++ 标准设置
不同 demo 需要不同标准(C++11 ~ C++23)。在 CMake 里推荐这样写:
1 | set(CMAKE_CXX_STANDARD 17) |
| 选项 | 含义 |
|---|---|
CMAKE_CXX_STANDARD |
目标 C++ 标准版本 |
CMAKE_CXX_STANDARD_REQUIRED ON |
编译器不支持该标准时失败,而不是静默降级 |
CMAKE_CXX_EXTENSIONS OFF |
禁用 GNU 扩展,保证可移植性 |
也可以在单个 target 上覆盖:
1 | target_compile_features(my_app PRIVATE cxx_std_20) |
fetch_content demo 使用 C++17,因为它依赖的 fmt 10.x 和 nlohmann/json 3.11 在 C++17 下体验最好。后面讲 C++20 特性的 demo 会把标准调到 20 或 23。
四、FetchContent:自动拉取依赖
手动管理第三方库的痛苦,做过的人都懂:下载 tarball、写 include 路径、处理版本冲突、CI 上还要再配一遍。
CMake 3.11 引入的 FetchContent 模块,可以在配置阶段自动下载并纳入构建:
1 | include(FetchContent) |
为什么用 URL 而不是 Git?
fetch_content demo 对所有依赖都用 URL + 版本 tag 的方式:
1 | FetchContent_Declare( |
| 方式 | 优点 | 缺点 |
|---|---|---|
| URL (tar.gz) | 不依赖 git;CI / 防火墙环境更友好 | 需要知道 release 包的 URL |
| Git 仓库 | 可以锁定 commit | 需要 git;浅克隆有时踩坑 |
demo 中的 6 个依赖
| 库 | 版本 | 用途 |
|---|---|---|
| Google Test | v1.14.0 | 单元测试(第 16 篇详讲) |
| nlohmann/json | v3.11.3 | JSON 解析 |
| fmt | 10.1.1 | 类型安全格式化 |
| spdlog | v1.13.0 | 高性能日志 |
| cpp-httplib | v0.15.3 | HTTP 客户端/服务端 |
| CLI11 | v2.4.1 | 命令行参数解析 |
一次性拉取全部依赖:
1 | FetchContent_MakeAvailable(googletest nlohmann_json fmt spdlog httplib CLI11) |
首次 ./build.sh 时,CMake 会下载这些库到 build/_deps/(已在 .gitignore 中忽略),之后增量编译不再重复下载。
关闭依赖自带的测试
很多库默认会构建自己的测试套件,拖慢编译。demo 里显式关闭:
1 | set(BUILD_GMOCK OFF CACHE BOOL "" FORCE) |
这是工程实践中的常见优化:只引入你需要的 target,关掉上游的 test / example。
五、目录组织:src / include / build
fetch_content 采用经典布局:
1 | projects/fetch_content/ |
CMake 里拆成库 + 可执行文件两个 target:
1 | add_library(my_lib STATIC src/mylib.cpp src/mylib.h) |
几个要点:
| 概念 | 说明 |
|---|---|
STATIC 库 |
编译期把代码链进最终二进制,demo 够用;大项目可换 SHARED |
PUBLIC include |
链接 my_lib 的目标自动获得头文件搜索路径 |
PUBLIC link |
依赖沿传递链传播——my_app 链 my_lib,就间接拥有 json/fmt 等 |
PRIVATE link |
仅自己用,不传播给下游 |
比「全局 include_directories + link_libraries」更安全:依赖关系绑定在具体 target 上,不会污染整个项目。
六、compile_commands.json 与 clangd
写 C++ 时,IDE 的跳转、补全、报错提示,依赖编译数据库。开启方式只需一行:
1 | set(CMAKE_EXPORT_COMPILE_COMMANDS ON) |
CMake 会在 build/ 下生成 compile_commands.json。demo 在根目录做了符号链接:
1 | compile_commands.json -> build/compile_commands.json |
配合 clangd(详见 ref/cpp_demo/docs/CPP_IDE_SETUP.md),打开 src/main.cpp 就能正确解析 <CLI/CLI.hpp> 等 FetchContent 下载的头文件。
提示:每次
build.sh -c清理重建后,如果 IDE 报找不到头文件,重启 clangd 或重新打开项目即可。
七、动手:编译并运行 fetch_content demo
进入 demo 目录,一键编译运行:
1 | cd ref/cpp_demo/projects/fetch_content |
首次运行会下载依赖,可能需要 1–3 分钟(视网络而定)。成功后终端大致输出:
1 | 欢迎使用 MyApp, World! 🎉 |
试试命令行参数
demo 用 CLI11 解析参数,可以玩几个组合:
1 | ./build.sh --run-args '--help' |
| 选项 | 作用 |
|---|---|
-n, --name |
设置用户名 |
-l, --log-level |
日志级别:debug / info / warn / error |
-c, --show-config |
打印当前配置 JSON |
--config |
从 JSON 字符串加载配置 |
代码里发生了什么?
main.cpp 把 6 个库串成一条演示链:
- CLI11 — 解析命令行,带类型检查和范围校验
- spdlog — 初始化彩色日志,按级别过滤输出
- fmt — 类型安全的字符串格式化(C++20 的
std::format同源思路) - nlohmann/json — 构建和打印 JSON 对象
- httplib — HTTP 客户端已集成(默认注释,避免无意发起网络请求)
mylib.cpp 里的 Config 类展示了库与库之间的协作:JSON 解析失败时,用 fmt 格式化错误信息,再用 spdlog 输出。
八、CMake 常见踩坑
| 现象 | 原因 | 处理 |
|---|---|---|
| 首次 cmake 很慢 | FetchContent 正在下载 | 正常,后续增量编译会快很多 |
找不到 fmt::fmt |
忘记 FetchContent_MakeAvailable |
声明后必须 MakeAvailable |
| clangd 报红但编译通过 | 索引未刷新 | 确认 compile_commands.json 链接存在 |
| C++20 demo 编译失败 | 标准版本不够 | 检查 CMAKE_CXX_STANDARD 与编译器版本 |
_deps/ 目录很大 |
缓存了所有下载的依赖 | build.sh -c 可清理;不影响源码 |
九、小结
本篇围绕 fetch_content demo,把现代 C++ 项目的构建链路串了一遍:
- CMake 三件套:
cmake_minimum_required→project→add_executable - C++ 标准:
CMAKE_CXX_STANDARD+REQUIRED防止静默降级 - FetchContent:URL 方式拉取依赖,关掉上游多余测试
- Target 模型:库与可执行文件分离,
PUBLIC/PRIVATE控制传播 - IDE 友好:
CMAKE_EXPORT_COMPILE_COMMANDS喂给 clangd
现代 C++ 实战系列第 1 篇完。下一篇我们聊 C++ 版本演进——从 C++03 到 C++23,各版本加了什么、为什么加,给后面每一篇特性文章一张路线图。
系列导航
| 篇号 | 标题 | 状态 |
|---|---|---|
| 00 | 环境搭建与项目导览 | ✅ |
| 01 | CMake 与现代构建(本篇) | ✅ |
| 02 | C++ 版本演进一览 | 下一篇 |
| 03 | 移动语义与右值引用 | 待写 |
完整大纲见工作区 docs/CPP_SERIES_OUTLINE.md。










