资讯动态

C++ Cmake使用详细教程(看这一篇就够了!)

发布时间:2026/10/21 22:08:46 来源:尧图企业网站定制
前言CMake 不是编译器也不是 C 标准的一部分而是一个“构建系统生成器”build system generator它读取CMakeLists.txt中的工程描述针对当前平台生成 Makefile、Ninja 构建文件或者 Visual Studio 工程再由底层工具真正调用编译器。很多初学者把它当成“更高级的 g 命令”于是写出只在某一台机器上成立的脚本也有人把它当成包管理器以为find_package会自动下载依赖。这两种理解都会在换平台、换编译器时出问题。第二个常见误解是版本。网上大量教程停留在 CMake 2.8/3.0 的写法全局include_directories、全局link_libraries、用file(GLOB ...)收集源文件。这些写法今天仍能工作但现代 CMake 的核心是“以 target 为中心”每个可执行文件、每个库都是一个 target头文件路径、编译选项、链接依赖都挂在 target 上并按PRIVATE、PUBLIC、INTERFACE三种可见性传播。本文以一个可构建、可测试、可安装的小工程为主线讲清CMakeLists.txt的组成、多目录组织、依赖查找和高频坑点。文中的行为按 CMake 3.16 及以上版本的语义描述不同版本在细节上有差异实际使用时以cmake --help和所用版本的官方文档为准。一、从一条编译命令到一份构建描述最原始的做法是把所有参数写在命令行里g -stdc17 -Iinclude src/main.cpp src/math.cpp -o calc这条命令在 Linux 上也许够用但它把编译器、标准版本、头文件路径、源文件列表、输出名全部硬编码在一起。源文件一多命令行会变得极长换到 MSVC-stdc17和-I的拼写又不一样想同时构建 Debug 和 Release只能自己复制命令。更麻烦的是增量构建每次都全量编译改一个文件也要等全部源文件重来。CMake 把这件事拆成三个阶段配置configure读取CMakeLists.txt检查编译器、查找依赖、把结果写入构建目录下的缓存文件CMakeCache.txt。生成generate根据配置结果生成具体构建系统文件例如build.ninja、Makefile或.sln。构建build调用底层构建工具由它决定哪些源文件需要重新编译、哪些可以复用目标文件。这三个阶段对应两条最常用的命令cmake -S . -B buildcmake --build build-S指定源码目录-B指定构建目录。这种“源码树外构建”out-of-source build把生成物全部放进build不会污染源码目录删掉build就等于彻底清理。老教程里常见的cmake .是源码内构建会把CMakeCache.txt、CMakeFiles混进源文件旁边不推荐。还要区分“单配置生成器”和“多配置生成器”。Unix Makefiles、Ninja 属于单配置配置时就要决定CMAKE_BUILD_TYPE是Debug还是Release一个构建目录只对应一种配置。Visual Studio、Xcode 属于多配置同一个工程里同时存在 Debug、Release 等配置构建时用--config选择。生成器类型选择配置的方式Unix Makefiles单配置配置时-DCMAKE_BUILD_TYPEReleaseNinja单配置配置时-DCMAKE_BUILD_TYPEReleaseVisual Studio 17 2022多配置构建时--config ReleaseXcode多配置构建时--config Release二、最小可用工程逐行读懂 CMakeLists.txt假设目录结构如下calc/├── CMakeLists.txt├── include/│ └── math.hpp└── src/├── main.cpp└── math.cpp一份现代写法的最小CMakeLists.txt# 适用于 CMake 3.16 及以上cmake_minimum_required(VERSION 3.16)project(Calc VERSION 1.0.0 LANGUAGES CXX)set(CMAKE_CXX_STANDARD 17)set(CMAKE_CXX_STANDARD_REQUIRED ON)set(CMAKE_CXX_EXTENSIONS OFF)add_executable(calc src/main.cpp src/math.cpp)target_include_directories(calc PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include)逐行说明cmake_minimum_required声明脚本需要的最低 CMake 版本。它不是“建议”而是策略开关低于该版本会直接报错高于该版本则按该版本引入的策略行为执行。写一个过低的版本号会静默采用旧策略埋下兼容性隐患。project定义工程名同时启用 CXX 语言。VERSION会生成PROJECT_VERSION等变量LANGUAGES CXX表示只启用 C避免在纯 C 工程里无谓地探测 C 编译器。CMAKE_CXX_STANDARD是目录属性影响此后创建的 target。CMAKE_CXX_STANDARD_REQUIRED ON表示编译器不支持该标准就报错而不是悄悄降级。CMAKE_CXX_EXTENSIONS OFF让编译器使用-stdc17而不是-stdgnu17避免依赖 GNU 扩展。add_executable定义可执行文件 target并列出源文件。注意这里只列.cpp头文件不需要列但可以列出来让 IDE 显示。target_include_directories给这个 target 添加头文件搜索路径。PRIVATE表示该路径只用于编译calc自身不向依赖它的 target 传播。对应的源码// 适用于 C17// include/math.hpp#pragma oncenamespace math {int add(int a, int b);int mul(int a, int b);}// 适用于 C17// src/math.cpp#include math.hppnamespace math {int add(int a, int b) {return a b;}int mul(int a, int b) {return a * b;}} // namespace math// 适用于 C17// src/main.cpp#include iostream#include math.hppint main() {int a 0;int b 0;std::cout 输入两个整数: ;if (!(std::cin a b)) {std::cerr 输入无效\n;return 1;}std::cout a b math::add(a, b) \n;std::cout a * b math::mul(a, b) \n;return 0;}构建时先配置再编译cmake -S . -B build -DCMAKE_BUILD_TYPEDebugcmake --build build单配置生成器下可执行文件通常位于build/calc多配置生成器下通常位于build/Debug/calc.exe。具体路径由生成器决定可以用--target和--config控制但不要假设它一定在某个固定位置。三、库、传播属性与依赖真实工程很少把所有代码塞进一个可执行文件。把可复用的部分做成库是 CMake 最核心的用法# 适用于 CMake 3.16 及以上add_library(math STATIC src/math.cpp)target_include_directories(mathPUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)target_compile_features(math PUBLIC cxx_std_17)add_executable(calc src/main.cpp)target_link_libraries(calc PRIVATE math)add_library的第二个参数可以是STATIC、SHARED也可以省略让BUILD_SHARED_LIBS决定。target_link_libraries不仅负责链接还负责把math的PUBLIC和INTERFACE属性传递给calc因为math的头文件路径是PUBLICcalc编译时会自动获得该路径不需要再写一遍target_include_directories。三种可见性的含义必须分清关键字作用于自身编译传递给依赖者典型用途PRIVATE是否内部实现用的头文件、只在 .cpp 里用的库PUBLIC是是公开头文件所在目录、公共依赖INTERFACE否是纯头文件库、只给使用者看的编译选项依赖第三方库时优先使用“导入目标”imported target而不是手写-l和-I# 适用于 CMake 3.16 及以上find_package(Threads REQUIRED)target_link_libraries(calc PRIVATE Threads::Threads)Threads::Threads是一个导入目标它会带上平台需要的链接选项在 Linux 上通常是-pthread在 Windows 上可能什么都不用加。手写-lpthread在 Linux 上也许能过在 Windows 上必然失败。find_package有两种模式MODULE模式查找FindXXX.cmakeCONFIG模式查找库自己安装的XXXConfig.cmake。后者通常更可靠因为它是库作者维护的。CMake 3.11 起提供了FetchContent3.14 起又加入了FetchContent_MakeAvailable可以在配置阶段拉取源码并作为子工程构建# 适用于 CMake 3.14 及以上include(FetchContent)FetchContent_Declare(googletestGIT_REPOSITORY https://github.com/google/googletest.gitGIT_TAG v1.14.0)FetchContent_MakeAvailable(googletest)这段写法依赖网络和 Git企业内网常常不可用生产项目更常见的做法是把依赖交给包管理器vcpkg、Conan或系统包管理器再由find_package接入。四、多目录、选项、测试与安装工程变大后每个子目录放一份CMakeLists.txt顶层用add_subdirectory组织# 适用于 CMake 3.16 及以上cmake_minimum_required(VERSION 3.16)project(Calc VERSION 1.0.0 LANGUAGES CXX)set(CMAKE_CXX_STANDARD 17)set(CMAKE_CXX_STANDARD_REQUIRED ON)set(CMAKE_CXX_EXTENSIONS OFF)option(CALC_BUILD_TESTS Build unit tests ON)add_subdirectory(src)if(CALC_BUILD_TESTS)enable_testing()add_subdirectory(tests)endif()install(TARGETS calc RUNTIME DESTINATION bin)option定义一个布尔缓存变量用户可以在配置时用-DCALC_BUILD_TESTSOFF覆盖。if判断后决定是否把测试目录加进来这样发布构建可以完全不编译测试代码。测试可以用 CTest 直接驱动一个自己写的断言程序不必引入第三方框架# 适用于 CMake 3.16 及以上# tests/CMakeLists.txtadd_executable(test_math test_math.cpp)target_link_libraries(test_math PRIVATE math)add_test(NAME math_add COMMAND test_math)// 适用于 C17// tests/test_math.cpp#include cassert#include math.hppint main() {assert(math::add(2, 3) 5);assert(math::mul(4, 5) 20);assert(math::add(-1, 1) 0);return 0;}配置时启用测试构建后运行cmake -S . -B build -DCMAKE_BUILD_TYPEDebugcmake --build buildctest --test-dir build --output-on-failure # --test-dir 需要 CMake 3.20 及以上若 CMake 版本低于 3.20可以进入构建目录后直接执行ctest --output-on-failure。注意assert在定义了NDEBUG的 Release 构建中会被展开为空所以断言只适合 Debug 自测。要写真正的单元测试应当使用专门的测试框架并让断言不依赖NDEBUG。安装规则把构建产物复制到前缀目录cmake --install build --prefix distinstall(TARGETS ... RUNTIME DESTINATION bin)中的RUNTIME对可执行文件有效库文件则用LIBRARY共享库和ARCHIVE静态库。在 Windows 上动态库的导入库属于ARCHIVEDLL 属于RUNTIME这三者经常被写混。常见坑点❌ 在add_executable之后才写set(CMAKE_CXX_STANDARD 17)以为能影响已经创建的 target。✅ 把标准设置放在所有add_*之前或者更精确地给每个 target 写target_compile_features(app PRIVATE cxx_std_17)。❌ 用file(GLOB SRCS src/*.cpp)收集源文件新增文件后不重新配置构建系统看不到新文件。✅ 显式列出源文件确实要通配时使用file(GLOB ... CONFIGURE_DEPENDS)但要清楚它在某些生成器上仍有性能与可靠性代价。❌ 用全局include_directories和link_libraries导致所有 target 共享路径依赖关系不可追踪。✅ 用target_include_directories、target_link_libraries并明确PRIVATE/PUBLIC/INTERFACE。❌ 在源码目录里执行cmake .生成物与源码混在一起清理困难。✅ 始终cmake -S 源码目录 -B 构建目录需要清理时直接删除构建目录。❌ 对 Visual Studio 生成器传-DCMAKE_BUILD_TYPERelease然后发现仍然是 Debug。✅ 多配置生成器用cmake --build build --config ReleaseCMAKE_BUILD_TYPE只对单配置生成器有意义。❌ 手写target_link_libraries(app PRIVATE pthread)或写死库文件路径换平台即失败。✅ 用find_package(Threads REQUIRED)加Threads::Threads其他库优先使用其导出的命名空间目标。❌ 认为cmake --build会自动重新运行配置修改CMakeLists.txt后行为诡异。✅ 正常构建会检测CMakeLists.txt变化并自动重配若怀疑缓存过期删除构建目录重新配置而不是手工改CMakeCache.txt。❌ 在target_link_libraries里把依赖写成PUBLIC把实现细节暴露给所有使用者。✅ 只在使用者也需要该依赖时才写PUBLIC否则一律PRIVATE纯头文件库才用INTERFACE。总结主题关键做法常见错误构建方式源码树外构建-S配-B在源码目录里cmake .标准版本早期统一设置或按 target 声明设置在 target 之后头文件路径target_include_directories全局include_directories依赖链接导入目标 可见性关键字手写-l、写死路径配置选择单配置用CMAKE_BUILD_TYPE多配置生成器也用它源文件列表显式列举无脑file(GLOB)CMake 的学习重点不是背命令而是理解“配置与生成分离”和“target 携带传播属性”这两个模型。把每个库、每个程序都当成独立 target把它的头文件、编译选项、依赖按可见性挂上去工程就能在不同编译器GCC、Clang、MSVC之间迁移而不必重写构建脚本。版本相关行为请以对应 CMake 版本的官方文档为准不要照搬十年前的教程。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑