构建系统:CMake 与包管理
掌握 CMake 基础、现代基于目标的 CMake、FetchContent 依赖管理、vcpkg 和 Conan 包管理器,以及安全和优化构建的编译器标志。
为什么构建系统很重要
随着 C++ 项目超出单个文件的规模,你需要一个构建系统来管理编译、链接、依赖和平台差异。手动运行 g++ *.cpp 无法扩展:你需要增量构建(仅重新编译已更改的文件)、库检测、跨平台支持和可重现的构建。CMake 已成为 C++ 事实上的标准构建系统生成器。它本身不编译代码 ── 它为你的平台生成原生构建文件(Makefile、Ninja 文件、Visual Studio 项目)。
CMake 基础
CMakeLists.txt 文件描述你的项目结构。现代 CMake(3.12+)使用基于目标的方法,所有内容都附加到目标(可执行文件或库)上:
# CMakeLists.txt — minimum viable project
cmake_minimum_required(VERSION 3.20)
project(MyApp VERSION 1.0.0 LANGUAGES CXX)
# Set C++ standard for the entire project
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_CXX_EXTENSIONS OFF) # use -std=c++20, not -std=gnu++20
# Create an executable target
add_executable(myapp
src/main.cpp
src/engine.cpp
src/utils.cpp
)
# Add include directories for this target
target_include_directories(myapp PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/include
)
# Create a library target
add_library(mathlib STATIC
src/math/vector.cpp
src/math/matrix.cpp
)
target_include_directories(mathlib PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/include/math
)
# Link the library to the executable
target_link_libraries(myapp PRIVATE mathlib)
# Compiler warnings — essential for quality code
target_compile_options(myapp PRIVATE
$<$<CXX_COMPILER_ID:GNU,Clang>:-Wall -Wextra -Wpedantic -Werror>
$<$<CXX_COMPILER_ID:MSVC>:/W4 /WX>
)使用 CMake 构建
CMake 使用两步流程:配置(生成构建文件)和构建(编译)。始终使用源外构建以保持源码树干净:
# Configure — generate build files in a 'build' directory
cmake -B build -DCMAKE_BUILD_TYPE=Release
# Build — compile the project
cmake --build build --parallel $(nproc)
# Install (optional)
cmake --install build --prefix /usr/local
# Common build types:
# Debug — -O0 -g (full debug symbols, no optimization)
# Release — -O3 -DNDEBUG (max optimization, no asserts)
# RelWithDebInfo — -O2 -g -DNDEBUG (optimized with debug info)
# MinSizeRel — -Os -DNDEBUG (optimize for size)
# Run with sanitizers (add to CMakeLists.txt or command line)
cmake -B build-asan -DCMAKE_CXX_FLAGS="-fsanitize=address,undefined -fno-omit-frame-pointer"FetchContent:依赖管理
CMake 的 FetchContent 模块在配置时下载和构建依赖,使得无需手动设置即可轻松引入库:
cmake_minimum_required(VERSION 3.20)
project(MyApp LANGUAGES CXX)
include(FetchContent)
# Fetch GoogleTest
FetchContent_Declare(
googletest
GIT_REPOSITORY https://github.com/google/googletest.git
GIT_TAG v1.14.0
)
# Fetch fmt (formatting library)
FetchContent_Declare(
fmt
GIT_REPOSITORY https://github.com/fmtlib/fmt.git
GIT_TAG 10.2.1
)
# Download and make available
FetchContent_MakeAvailable(googletest fmt)
add_executable(myapp src/main.cpp)
target_link_libraries(myapp PRIVATE fmt::fmt)
# Test executable
enable_testing()
add_executable(tests tests/test_main.cpp)
target_link_libraries(tests PRIVATE GTest::gtest_main)
add_test(NAME unit_tests COMMAND tests)vcpkg 和 Conan
对于有许多依赖的大型项目,专用的 C++ 包管理器提供更好的依赖解析和缓存:
vcpkg(Microsoft)── 通过工具链文件直接与 CMake 集成。全局安装包或在「清单模式」中使用 vcpkg.json 文件:
{
"dependencies": ["fmt", "spdlog", "nlohmann-json", "catch2"]
}使用方式:cmake -B build -DCMAKE_TOOLCHAIN_FILE=$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake
Conan ── 使用 conanfile.txt 或 conanfile.py 声明依赖。支持多种构建配置和二进制缓存:
[requires]
fmt/10.2.1
spdlog/1.13.0
[generators]
CMakeDeps
CMakeToolchain两个包管理器都维护着包含数千个 C++ 库的仓库,提供经过测试的预构建二进制文件。
遵循以下规则编写可维护的 CMake:
1. 始终使用基于目标的命令:target_include_directories、target_compile_options、target_link_libraries ── 绝不使用 include_directories 或 add_definitions(它们会污染全局作用域)。
2. 理解 PRIVATE/PUBLIC/INTERFACE:PRIVATE = 仅用于此目标,PUBLIC = 用于此目标及其消费者,INTERFACE = 仅用于消费者。
3. 按目标或项目级别设置 C++ 标准,绝不通过原始标志如 -std=c++20。
4. 启用最多警告:-Wall -Wextra -Wpedantic(GCC/Clang)或 /W4(MSVC)。在 CI 中考虑使用 -Werror / /WX。
5. 使用 cmake --preset 实现可重现的构建(CMakePresets.json)。
6. 绝不硬编码路径 ── 使用 find_package、FetchContent 或包管理器。
使用 file(GLOB ...) 收集源文件:file(GLOB SOURCES src/*.cpp) 在添加或删除文件时不会重新运行。新文件在你手动重新配置之前不会被编译。始终显式列出源文件。
忘记 CMAKE_CXX_EXTENSIONS OFF:没有这个设置,GCC 使用 -std=gnu++20 而不是 -std=c++20,启用非标准扩展导致可移植性问题。
混用 add_subdirectory 和 FetchContent:两者都引入外部项目,但它们与构建缓存的交互方式不同。每个依赖选择一种方法。
未设置 CMAKE_EXPORT_COMPILE_COMMANDS ON:这会生成 compile_commands.json,clang-tidy、clangd(LSP)和许多 IDE 集成都需要它。始终启用它。
- CMake 是标准的 C++ 构建系统生成器 ── 学习其基于目标的 API
- 使用
target_*命令(非全局作用域)配合 PRIVATE/PUBLIC/INTERFACE 可见性 - FetchContent 在配置时下载和构建依赖 ── 适合小型依赖集
- vcpkg(清单模式)和 Conan 处理更大的依赖图并提供二进制缓存
- 始终启用警告(
-Wall -Wextra -Wpedantic)并使用源外构建 - 生成
compile_commands.json供 IDE 和静态分析工具使用
测验 — 检验你的知识
(15 经验值)1. `target_link_libraries(myapp PRIVATE mathlib)` 做了什么?
2. 为什么 `file(GLOB SOURCES src/*.cpp)` 在 CMake 中被认为是不好的做法?
3. `CMAKE_CXX_EXTENSIONS OFF` 的目的是什么?