尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

CMake 入门:从单文件到多目标工程

发布时间:2026/9/26 4:04:54

资讯中心
01
ARTICLE

CMake 入门:从单文件到多目标工程

CMake 入门:从单文件到多目标工程
一个.cpp文件时g main.cpp -o app就够了等到工程变成「一个静态库 两个可执行文件 一套测试 一个第三方依赖」手写编译命令就会迅速失控——你开始记不住该编哪些文件、按什么顺序链、哪些-I路径给谁。构建系统要解决的就是这件事而 CMake 是 C 世界事实上的标准。这篇的目标很简单给你一份能直接抄走的多目标工程模板并把现代 CMake 唯一必须搞懂的概念——PUBLIC / PRIVATE / INTERFACE 的传播语义——讲透。1. 引子为什么手写编译命令必然失控三个具体问题多文件编译改一个.cpp只需要重编它自己其余复用已有目标文件——这是靠「时间戳 依赖图」实现的手写命令做不到。依赖管理app依赖mathlibmathlib依赖fmt。谁先编、谁链谁、头文件路径给谁——这是一张有向图不是一条命令行。跨平台Linux 用g、macOS 用clang、Windows 用 MSVC编译选项、库后缀、可执行文件后缀全都不同。构建系统把「我要什么」和「这台机器上怎么做」拆开。官方文档translation phases翻译阶段——标准把「一个 .cpp 编译成一个翻译单元」定义在第 8 阶段这是「多文件独立编译 最后链接」的理论基础。CMake 不是编译器它是构建系统的生成器你写CMakeLists.txt描述工程结构CMake 生成 Makefile / Ninja 文件 / Visual Studio 工程再交给真正的构建工具去跑。官方文档CMake 官方教程——跟着做一遍比读十篇博客有用。2. 现代 CMake 的核心理念以 target 为中心这是全文最重要的一句话不要问「这个目录下要加什么编译选项」要问「这个 target 需要什么」。老式写法用全局命令一次调用影响后面所有target老写法全局已不推荐现代写法target 版老写法的问题include_directories(include)target_include_directories(tgt PUBLIC include)污染目录下所有 target且无法表达「这个路径该不该传给消费者」link_libraries(fmt)target_link_libraries(tgt PRIVATE fmt)同理链接依赖变得不可追踪add_definitions(-DFOO)target_compile_definitions(tgt PRIVATE FOO)宏会泄漏给无关 target手改CMAKE_CXX_FLAGStarget_compile_features/target_compile_options全局改标准容易互相打架且无法按 target 区分差别不只是「风格」而是依赖关系能不能被表达和检查target 版的写法让「谁需要什么」直接写在图里CMake 可以据此算出正确的编译顺序、正确的-I和正确的链接行全局写法只能一股脑塞给所有人。官方文档cmake-buildsystem(7)目标与依赖图3. 工程目录结构先看一个真实可用的多目标工程长什么样myproj/ ├── CMakeLists.txt # 顶层只做全局配置 组织子目录不写具体编译细节 ├── mathlib/ │ ├── CMakeLists.txt # 静态库目标 mathlib │ ├── include/ │ │ └── mathlib/ │ │ └── mathlib.h ← 公开头文件消费者 #include mathlib/mathlib.h │ └── src/ │ ├── mathlib.cpp ← 实现 │ └── internal.h ← 私有头文件只有 mathlib 自己能看见 ├── src/ │ ├── CMakeLists.txt # 可执行目标 greet │ └── main.cpp ├── tests/ │ ├── CMakeLists.txt # 测试目标 test_mathlib │ └── test_mathlib.cpp └── build/ # 构建目录不进版本库要点公开头文件放include/私有实现头放src/。这个物理隔离不是洁癖——它是 PUBLIC / PRIVATE 能生效的前提一旦实现头文件和公开头文件混在一起消费者就总能顺着-I摸到你的内部实现接口边界立刻失守。4. 最小可用模板顶层 CMakeLists.txtcmake_minimum_required(VERSION3.16)# 3.16 起 target_link_libraries 的传播语义才足够稳定project(myproj VERSION0.1.0 LANGUAGES CXX)# C 标准全局只声明「最低要求」具体由 target_compile_features 传播set(CMAKE_CXX_STANDARD17)set(CMAKE_CXX_STANDARD_REQUIRED ON)set(CMAKE_CXX_EXTENSIONS OFF)# 用 -stdc17而不是 gnu17# 单配置生成器Unix Makefiles / Ninja下不设 BUILD_TYPE 就是「无优化、无调试信息」if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES)set(CMAKE_BUILD_TYPE Debug CACHE STRING构建类型FORCE)endif()add_subdirectory(mathlib)add_subdirectory(src)enable_testing()add_subdirectory(tests)四个必需元素各自的职责cmake_minimum_required声明最低版本决定可用的命令与策略默认值、project声明工程名与语言、add_subdirectory把子目录挂进依赖图、enable_testing打开ctest支持。CMAKE_CXX_EXTENSIONS OFF值得单独记一下不关掉它GCC/Clang 会用-gnu17你可能在不经意间用上 GNU 扩展换到 MSVC 就编不过。官方文档Core Guidelines P.2Write in ISO Standard C——「只用标准 C」这条规则落到构建脚本上就是CMAKE_CXX_EXTENSIONS OFF。官方文档cmake_minimum_required、project5. 静态库目标PUBLIC / PRIVATE 的传播语义这是现代 CMake 最容易搞混、也最值得花时间理解的概念。先看库的CMakeLists.txtadd_library(mathlib STATIC src/mathlib.cpp)# 显式列出源文件不要用 file(GLOB)target_include_directories(mathlib PUBLIC${CMAKE_CURRENT_SOURCE_DIR}/include# 消费者也要能 #includePRIVATE${CMAKE_CURRENT_SOURCE_DIR}/src)# 私有实现头不外传# 把「我至少需要 C17」这件事传播给消费者而不是硬编码 -stdc17target_compile_features(mathlib PUBLIC cxx_std_17)# 警告选项只加给这个 target不污染整个工程target_compile_options(mathlib PRIVATE-Wall-Wextra)三个关键字的语义用一张图理解最直观依赖是怎么沿 target 传播的 ═══════════════════════════════════════════════════════════════════════ ① PRIVATE只有我自己用消费者看不见 app ──链接──▶ mathlib ├── PRIVATE include 路径 ──▶ mathlib 自己编译时用 └── ✗ 不传播 ──▶ app 的编译命令里没有这条 -I ② INTERFACE我自己不用但消费者必须继承 app ──链接──▶ headeronly纯头文件库 └── INTERFACE include 路径 ──▶ 只加到 app 的 -I 上 ③ PUBLIC自己要用消费者也要继承 PRIVATE INTERFACE app ──链接──▶ mathlib ├── 先用在自己身上 └── PUBLIC include 路径 ──▶ 同时加到 app 的 -I 上 并且继续向下传 传播方向mathlib ──▶ 它的消费者app、test_mathlib──▶ 消费者的消费者 链接关系是「向下游传递属性」不是「向上游查找」换成决策表你想表达的意思该用哪个关键字典型写法这是我自己的实现细节别人不该知道PRIVATEtarget_include_directories(mathlib PRIVATE src)这是我的公开接口用我的人必须能看到PUBLICtarget_include_directories(mathlib PUBLIC include)我只是个纯头文件库本身不需要编译INTERFACEadd_library(hdr INTERFACE)target_include_directories(hdr INTERFACE include)可执行文件链接库终点不向下传PRIVATEtarget_link_libraries(greet PRIVATE mathlib)我依赖的第三方库也是我接口的一部分PUBLICtarget_link_libraries(mathlib PUBLIC fmt::fmt)一句话记忆法PRIVATE 是「我的事」INTERFACE 是「用我的人的事」PUBLIC 是「两边都有的事」。判断标准只有一个问题「消费者需不需要知道这件事」官方文档target_include_directories、target_link_libraries仔细看 PUBLIC/PRIVATE/INTERFACE 一节6. 可执行目标与测试目标src/CMakeLists.txtadd_executable(greet main.cpp)# greet 是终点没人链接它所以用 PRIVATEtarget_link_libraries(greet PRIVATE mathlib)target_compile_options(greet PRIVATE-Wall-Wextra)tests/CMakeLists.txtadd_executable(test_mathlib test_mathlib.cpp)target_link_libraries(test_mathlib PRIVATE mathlib)# 注册到 ctest跑 ctest 时会执行它返回非 0 即判定失败add_test(NAME mathlib_basic COMMAND test_mathlib)三个目标对应的源码各跑一次确认语义没写错// src/main.cpp — g -stdc17 -Wall -O2 src/main.cpp -o greet#includeiostreamintmain(){std::couthello from cmake\n;}hello from cmake// src/main.cpp — g -stdc17 -Wall -O2 src/main.cpp -o greet// 真实工程里 add/sub 由静态库 mathlib 提供#include mathlib/mathlib.h// 这里为了让示例能独立编译运行直接把实现放在同一个文件里。#includeiostreamnamespacemathlib{intadd(inta,intb){returnab;}intsub(inta,intb){returna-b;}}// namespace mathlibintmain(){std::coutadd(2, 3) mathlib::add(2,3)\n;std::coutsub(2, 3) mathlib::sub(2,3)\n;}add(2, 3) 5 sub(2, 3) -1// tests/test_mathlib.cpp — ctest 会执行它返回非 0 即判定失败#includeiostream// 真实工程里改为 #include mathlib/mathlib.h 并链接 mathlib 目标// 这里为了让示例能独立编译运行直接给出等价实现。constexprintadd(inta,intb){returnab;}intmain(){intfailures0;if(add(2,3)!5){std::cout[FAIL] add(2, 3) 应为 5\n;failures;}if(add(-1,1)!0){std::cout[FAIL] add(-1, 1) 应为 0\n;failures;}if(failures0){std::cout[PASS] 2 个用例全部通过\n;}else{std::cout[FAIL] 有 failures 个用例失败\n;}returnfailures0?0:1;}[PASS] 2 个用例全部通过测试目标的价值在于它把「库被正确导出」这件事也一并验证了如果mathlib的 include 路径被误写成PRIVATEtest_mathlib.cpp会因为找不到mathlib/mathlib.h而编译失败——错误在构建期就暴露而不是等到下游用户投诉。官方文档add_test、enable_testing7. 构建类型CMAKE_BUILD_TYPE到底改了什么# 配置只跑一次 构建每次改动后跑cmake-S.-Bbuild-DCMAKE_BUILD_TYPEDebug cmake--buildbuild-j# 之后想换构建类型改配置即可不要手动 rm -rf buildcmake-S.-Bbuild-DCMAKE_BUILD_TYPERelease各档位对应的默认编译选项GCC 风格构建类型优化调试信息额外宏适用场景不设置无-O0无无不推荐既没优化也没调试信息纯属自找麻烦Debug无-O0有-g无日常开发、断点跟踪、断言生效Release有-O3无-DNDEBUG发布产物RelWithDebInfo有-O2有-g-DNDEBUG线上抓栈、性能分析推荐给压测MinSizeRel体积优先-Os无-DNDEBUG嵌入式 / 体积敏感两个容易踩的点NDEBUG是Release系列自动加上的所以assert在 Release 下会整体消失——这正是断言里绝不能放副作用的原因详见《assert 与 static_assert把假设写进代码》。多配置生成器Visual Studio、Ninja Multi-Config会忽略CMAKE_BUILD_TYPE改用cmake --build build --config Release。写跨平台脚本时这里必须区别对待。用一个程序直观验证宏差异// src/which_build.cpp — 观察 CMAKE_BUILD_TYPE 带来的宏差异#includeiostreamintmain(){#ifdefNDEBUGstd::cout构建倾向Release 系列已定义 NDEBUGassert 会消失\n;#elsestd::cout构建倾向Debug未定义 NDEBUGassert 生效\n;#endif}构建倾向Debug未定义 NDEBUGassert 生效上面这次运行没有传-DNDEBUG所以走的是Debug分支同一个可执行文件在-DCMAKE_BUILD_TYPERelease的构建目录里跑就会打印另一行。官方文档CMAKE_BUILD_TYPE——只看这篇别信「Release 就是 -O2」这种以讹传讹的说法。8. 别用file(GLOB)收集源码这条是 CMake 官方文档里明确写着的建议却也是最经典的坑# 反例不要这么写新增 .cpp 文件不会触发重新配置file(GLOB SRC_FILESsrc/*.cpp)add_executable(app${SRC_FILES})原因file(GLOB)是在配置阶段执行的结果被缓存进构建目录。当你新建一个src/extra.cpp再跑cmake --build buildCMake不会重跑配置它只会检查CMakeLists.txt有没有变于是新文件根本不会进入编译列表。表现形式极其迷惑代码明明写了函数却「未定义」重启 IDE 又好了。正确做法是显式列出源文件add_executable(app main.cpp extra.cpp)# 新增文件时手动加一行改动会让 CMake 自动重跑配置多写一行换来的是可预测性文件列表的变化永远经过你手构建结果不会因为「缓存忘了刷新」而漂移。CONFIGURE_DEPENDS选项可以缓解但它靠每次构建时遍历目录来判断既慢又不完全可靠不如直接列出来。官方文档file(GLOB) 的官方说明——原文写着「We do not recommend using GLOB to collect a list of source files」。9.find_package引入第三方库点到为止标准库之外的依赖现代 CMake 的统一入口是find_package 命名空间化的 imported targetfind_package(Threads REQUIRED)# 编译器自带的线程库几乎总能用target_link_libraries(greet PRIVATE Threads::Threads)find_package(fmt CONFIG REQUIRED)# 第三方库提供的 config 包target_link_libraries(greet PRIVATE fmt::fmt)# 直接用 fmt::fmt不要自己拼 -I / -l关键点fmt::fmt这样的「命名空间化 target」会把该库需要的 include 路径、编译选项、传递依赖一起带过来你不用关心它装在哪。这就是 target 为中心的好处——第三方库也遵守同一套传播规则。REQUIRED表示找不到就报错停止配置比默默继续、最后在链接期炸掉好得多。CONFIG表示使用库自己安装的*Config.cmake是现代库的推荐方式。官方文档find_package10. 常用编译选项怎么加才对# 只加给一个 target不污染别人target_compile_options(mathlib PRIVATE-Wall-Wextra)# 跨编译器的情况MSVC 不认识 -Wall / -Wextratarget_compile_options(mathlib PRIVATE $$CXX_COMPILER_ID:GNU,Clang,AppleClang:-Wall;-Wextra$$CXX_COMPILER_ID:MSVC:/W4)# 「我需要 C17 的哪个特性」——用 feature 名而不是硬编码 -stdtarget_compile_features(mathlib PUBLIC cxx_std_17)# 需要某个具体特性时写具体名字消费者的标准会被自动抬到满足它target_compile_features(greet PRIVATE cxx_std_17)$...是生成器表达式generator expression它在生成阶段而不是配置阶段求值所以能根据实际编译器/构建类型切换选项。target_compile_features比硬编码-stdc17更好因为它是声明式的库说「我至少要 C17」CMake 负责把消费者的标准抬到够用而不是让两边互相覆盖。官方文档target_compile_features、生成器表达式每个cxx_std_17这类 feature 名都对应一组标准库/语言要求的特性测试宏feature-test macro。想知道某个特性名字覆盖了什么或者想在自己的头文件里用__cpp_*宏做条件编译看这两篇官方文档feature-test macros、编译器特性支持表11. 实测真跑一遍上面的多目标工程上面这些CMakeLists.txt不是示意——把「静态库 可执行文件 测试」三个 target 放进一个工程真构建一遍日志长这样Compiler Explorer 的 CMake 工程gcc 13.2-- The CXX compiler identification is GNU 13.2.0 -- Configuring done (0.3s) -- Generating done (0.0s) -- Build files have been written to: /app/build [ 16%] Building CXX object CMakeFiles/mathlib.dir/mathlib.cpp.o [ 33%] Linking CXX static library libmathlib.a [ 33%] Built target mathlib [ 50%] Building CXX object CMakeFiles/greet.dir/main.cpp.o [ 66%] Linking CXX executable greet add(2, 3) 5 ← 构建完直接跑 greet 的输出 [ 83%] Building CXX object CMakeFiles/test_mathlib.dir/test_mathlib.cpp.o [100%] Linking CXX executable test_mathlib [100%] Built target test_mathlib说明在线沙箱的文件是扁平的所以这里把add_subdirectory的目录结构拍平成单个CMakeLists.txtadd_library/add_executable/add_test的语义完全一致。值得盯着看的是构建顺序CMake 自己算出了依赖——先编mathlib再编依赖它的greet和test_mathlib最后各自链接。这就是第 1 节说的「依赖图」你只声明依赖顺序交给它。12. 延伸阅读CMake 官方教程从最小工程推到完整多目标工程官方维护跟着敲最省事cmake-buildsystem(7)target、属性、传播语义的权威定义PUBLIC/PRIVATE 讲不清时回这里target_link_libraries传播语义的逐条说明CMAKE_BUILD_TYPE各档默认选项与「多配置生成器会忽略它」的说明cppreference编译器特性支持表用target_compile_features之前先确认目标编译器真的支持这个特性Compiler Explorer想确认 CMake 生成的选项到底会产出什么代码把选项抄进 godbolt 看一眼13. 一句话总结现代 CMake 只有一条主线以 target 为中心——add_library/add_executable定义 targettarget_include_directories/target_link_libraries/target_compile_features用 PUBLIC我用、消费者也用、PRIVATE只有我用、INTERFACE只有消费者用声明依赖怎么传播源码显式列出不用file(GLOB)构建类型用-DCMAKE_BUILD_TYPE指定并记住 Release 会自动带上NDEBUG。把这几条做对多目标工程的结构就不会失控。
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。