上一篇我们把环境跑通了。从这一篇开始,我们真正进入 C++ 工程的地基:CMake。本系列 41 个 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
2
3
4
cmake_minimum_required(VERSION 3.20...4.2)
project(Hello VERSION 1.0.0)

add_executable(hello src/main.cpp)
指令 作用
cmake_minimum_required 声明所需最低 CMake 版本,低于此版本直接报错;3.20...4.2 这种区间写法表示「最低 3.20,按 4.2 的策略行为运行」,新版 CMake 4 下不会触发兼容性警告
project 定义项目名、版本,并初始化编译器检测
add_executable 从源文件生成可执行目标

典型工作流:

1
2
3
4
mkdir build && cd build
cmake .. # 配置:生成构建文件
cmake --build . -j$(nproc) # 编译:并行构建
./hello # 运行

本系列 demo 把上述步骤封装进了 ./build.sh,等价于:

1
2
3
4
cd ref/cpp_demo/projects/fetch_content
./build.sh # 增量编译
./build.sh -c # 清理后重建
./build.sh --run # 编译后运行

所有 demo 的 build.sh 都由仓库根目录的 scripts/build.sh.template 生成,参数完全一致(-s 开启 Sanitizer、--cxx 切换编译器等见第 00 篇)。

三、C++ 标准设置

不同 demo 需要不同标准(C++11 ~ C++23)。在 CMake 里推荐这样写:

1
2
3
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF)
选项 含义
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,后面讲 C++20 特性的 demo 会把标准调到 20 或 23。注意这三行要写在 FetchContent_MakeAvailable 之前,保证第三方库与主程序用同一个标准编译。

四、FetchContent:自动拉取依赖

手动管理第三方库的痛苦,做过的人都懂:下载 tarball、写 include 路径、处理版本冲突、CI 上还要再配一遍。

CMake 3.11 引入的 FetchContent 模块,可以在配置阶段自动下载并纳入构建:

1
2
3
4
5
6
7
8
9
10
11
include(FetchContent)

FetchContent_Declare(
fmt
URL https://github.com/fmtlib/fmt/archive/refs/tags/11.2.0.tar.gz
)

FetchContent_MakeAvailable(fmt)

add_executable(demo main.cpp)
target_link_libraries(demo PRIVATE fmt::fmt)

为什么用 URL 而不是 Git?

fetch_content demo 对所有依赖都用 URL + 版本 tag 的方式:

1
2
3
4
FetchContent_Declare(
nlohmann_json
URL https://github.com/nlohmann/json/archive/refs/tags/v3.12.0.tar.gz
)
方式 优点 缺点
URL (tar.gz) 不依赖 git;CI / 防火墙环境更友好 需要知道 release 包的 URL
Git 仓库 可以锁定 commit 需要 git;浅克隆有时踩坑

demo 中的 6 个依赖

库 版本 用途
Google Test v1.17.0 单元测试(第 16 篇详讲)
nlohmann/json v3.12.0 JSON 解析
fmt 11.2.0 类型安全格式化
spdlog v1.15.3 高性能日志
cpp-httplib v0.58.0 HTTP 客户端/服务端
CLI11 v2.5.0 命令行参数解析

版本提示:fmt 10.x 在新版 Clang 的 C++20 模式下会触发 consteval 编译错误,nlohmann/json 3.11 的 cmake_minimum_required 与 CMake 4 不兼容,所以 demo 统一升级到了上表版本。锁定版本号的好处就在这里——升级是一次显式、可回溯的改动。

一次性拉取全部依赖:

1
FetchContent_MakeAvailable(googletest nlohmann_json fmt spdlog httplib CLI11)

首次 ./build.sh 时,CMake 会下载这些库到 build/_deps/(已在 .gitignore 中忽略),之后增量编译不再重复下载。

关闭依赖自带的测试

很多库默认会构建自己的测试套件,拖慢编译。demo 里显式关闭:

1
2
3
set(BUILD_GMOCK OFF CACHE BOOL "" FORCE)
set(JSON_BuildTests OFF CACHE BOOL "" FORCE)
set(FMT_TEST OFF CACHE BOOL "" FORCE)

这是工程实践中的常见优化:只引入你需要的 target,关掉上游的 test / example。

同理,cpp-httplib 默认会「找到就用」系统里的 OpenSSL / zlib / brotli / zstd,导致 macOS 与 Ubuntu 编出来的功能不一致。demo 显式关掉用不到的可选依赖:

1
2
3
4
set(HTTPLIB_USE_OPENSSL_IF_AVAILABLE OFF CACHE BOOL "" FORCE)
set(HTTPLIB_USE_ZLIB_IF_AVAILABLE OFF CACHE BOOL "" FORCE)
set(HTTPLIB_USE_BROTLI_IF_AVAILABLE OFF CACHE BOOL "" FORCE)
set(HTTPLIB_USE_ZSTD_IF_AVAILABLE OFF CACHE BOOL "" FORCE)

五、目录组织:src / include / build

fetch_content 采用经典布局:

1
2
3
4
5
6
7
8
9
10
11
projects/fetch_content/
├── CMakeLists.txt # 顶层构建配置
├── build.sh # 统一构建脚本
├── README.md # 项目说明
├── .gitignore # 忽略 build/、_deps/ 等
├── compile_commands.json # → build/ 的符号链接
├── build/ # 构建产物(不提交 Git)
└── src/
├── mylib.h # 库头文件
├── mylib.cpp # 库实现
└── main.cpp # 程序入口

CMake 里拆成库 + 可执行文件两个 target:

1
2
3
4
5
6
7
8
9
10
11
12
add_library(my_lib STATIC src/mylib.cpp src/mylib.h)
target_include_directories(my_lib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/src)
target_link_libraries(my_lib PUBLIC
nlohmann_json::nlohmann_json
fmt::fmt
spdlog::spdlog_header_only
httplib::httplib
CLI11::CLI11
)

add_executable(my_app src/main.cpp)
target_link_libraries(my_app PRIVATE my_lib)

几个要点:

概念 说明
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
2
cd ref/cpp_demo/projects/fetch_content
./build.sh --run

首次运行会下载依赖,可能需要 1–3 分钟(视网络而定)。成功后终端大致输出(时间戳因运行而异;日志用 spdlog::info 直接输出,没有源码位置信息,所以 [%s:%#] 显示为 [:]):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
[2026-06-14 14:00:00.123] [info] [:] 日志系统已初始化,级别: info

欢迎使用 MyApp, World! 🎉

[2026-06-14 14:00:00.123] [info] [:] JSON 库使用示例:
示例 JSON 数据:
{
"dependencies": {
"CLI11": "v2.5.0",
...
},
"features": [
"FetchContent",
"CMake",
"Modern C++"
],
"name": "World"
}

[2026-06-14 14:00:00.123] [info] [:] fmt 格式化示例:
整数格式化: 42
浮点格式化: 3.142
...

注意 JSON 的键是按字母序输出的——nlohmann::json 默认用 std::map 存对象,不保留插入顺序。

试试命令行参数

demo 用 CLI11 解析参数,可以玩几个组合:

1
2
3
4
./build.sh --run-args '--help'
./build.sh --run-args '--show-config'
./build.sh --run-args '-n Developer -l debug'
./build.sh --run-args '--config {"app_name":"TestApp","port":3000}'
选项 作用
-n, --name 设置用户名
-l, --log-level 日志级别:debug / info / warn / error
-p, --port 服务端口,默认 8080,范围 1–65535
-c, --show-config 打印当前配置 JSON
--config 从 JSON 字符串加载配置

代码里发生了什么?

main.cpp 把其中 5 个库串成一条演示链(Google Test 只随 FetchContent 一起拉取,本 demo 没有编写测试 target):

  1. CLI11 — 解析命令行,带类型检查和范围校验
  2. spdlog — 初始化彩色日志,按级别过滤输出
  3. fmt — 类型安全的字符串格式化(C++20 的 std::format 同源思路),欢迎语还用了 fmt/color.h 的粗体青色输出
  4. nlohmann/json — 构建和打印 JSON 对象
  5. httplib — mylib::httpGet(host, path) 已封装好 HTTP GET,main.cpp 里的调用默认注释掉,避免无意发起网络请求

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 移动语义与右值引用 ✅