CMake 基础(现代 C++ 工程面试笔记)
面试回答
常见问法
- CMake 是干什么的?
- CMake 和 Make / Ninja / 编译器是什么关系?
- 为什么现代 C++ 工程面试经常会问 CMake?
- 什么是 target-based CMake?
target_include_directories、target_link_libraries、target_compile_definitions为什么都强调“挂到目标上”?PUBLIC、PRIVATE、INTERFACE分别是什么意思?- 第三方库在 CMake 里通常怎么接入?
- 为什么不推荐
include_directories()、link_libraries()这种全局写法?
回答
CMake 不是编译器,也不是直接执行编译的工具,它本质上是一个构建系统生成器。 它负责描述工程里的:
- 有哪些构建目标(可执行文件、静态库、动态库、接口库)
- 目标之间的依赖关系
- 头文件搜索路径
- 编译选项、宏定义、语言标准
- 链接关系
- 第三方库接入方式
然后根据平台和生成器,生成对应的构建文件,比如 Makefiles、Ninja、Visual Studio 工程等,再由底层构建工具真正去调用编译器完成编译。
现代 C++ 面试问 CMake,重点通常不在背命令,而在看你有没有工程意识。核心看三点:
- 你是否理解“目标”是构建的基本单位
- 你是否会正确表达依赖和使用要求(usage requirements)
- 你是否知道 include、宏、编译选项、链接库应该跟着目标走,而不是全局乱撒
一个最小示例可以这样写:
cmake_minimum_required(VERSION 3.20)
project(MyApp LANGUAGES CXX)
add_executable(app main.cpp)
target_compile_features(app PRIVATE cxx_std_20)
如果再稍微工程化一点:
cmake_minimum_required(VERSION 3.20)
project(MyApp LANGUAGES CXX)
add_library(core STATIC core.cpp)
target_include_directories(core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
target_compile_features(core PUBLIC cxx_std_20)
add_executable(app main.cpp)
target_link_libraries(app PRIVATE core)
这里的意思是:
core是一个库目标- 它公开暴露了头文件目录
include - 它要求使用方至少支持 C++20
app依赖core- 因为
core的 include 和编译特性是PUBLIC,所以这些要求会传递给app
一句话总结: 现代 CMake 的本质,是把构建信息准确地绑定到目标,并通过依赖关系让这些信息按规则传播。
追问
1)为什么现代 CMake 强调 target-based 写法?
因为它更符合真实工程依赖模型:
- 依赖是谁,就把属性挂给谁
- 谁需要这个头文件目录、宏、编译选项,就由谁声明
- 依赖传播可控,边界清晰
- 多模块工程更容易维护
- 第三方库接入更规范
全局写法的问题是:
- 污染范围大
- 很难定位某个编译选项是从哪来的
- 子目录/多模块之间容易相互影响
- 大工程里会越来越难维护
2)PUBLIC、PRIVATE、INTERFACE 怎么理解?
这是 CMake 里最常被追问的点,本质是在描述**“当前目标自己要不要用,以及依赖它的目标要不要继承”**。
PRIVATE:只给当前目标自己用,不向外传播PUBLIC:当前目标自己用,依赖它的目标也要继承INTERFACE:当前目标自己不用,但依赖它的目标要继承
例如:
add_library(core STATIC core.cpp)
target_include_directories(core PUBLIC include)
target_compile_definitions(core PRIVATE CORE_ENABLE_LOG)
解释:
include是core对外接口的一部分,所以用PUBLICCORE_ENABLE_LOG只是core内部实现细节,所以用PRIVATE
如果是一个纯头文件库,常见写法是:
add_library(utils INTERFACE)
target_include_directories(utils INTERFACE include)
target_compile_features(utils INTERFACE cxx_std_20)
因为它没有自己的 .cpp 要编译,所以它自己“不用”,只需要把使用要求传给依赖方。
3)CMake、Make、Ninja、编译器分别是什么关系?
可以这样回答:
- CMake:生成构建系统
- Make/Ninja/MSBuild:执行构建
- g++/clang++/MSVC:真正编译和链接代码
也就是说:
CMake 描述工程,生成构建文件;底层构建工具执行这些文件;编译器最终完成编译。
4)第三方库通常怎么接入?
现代 CMake 里优先用“目标”方式接入第三方库,而不是手写裸路径。
常见方式有:
find_package()找已安装包add_subdirectory()引入源码依赖FetchContent拉取源码依赖
例如:
find_package(fmt REQUIRED)
add_executable(app main.cpp)
target_link_libraries(app PRIVATE fmt::fmt)
这里最重要的不是记住 fmt,而是要体现一个思想:
第三方库也最好以“目标”的形式使用,因为它会自带 include、链接信息、编译要求,接入更稳。
原理展开
1. CMake 的核心不是“命令”,而是“构建模型”
很多人初学 CMake 时容易把注意力放在命令本身,比如背:
add_executableinclude_directorieslink_directoriesset(CMAKE_CXX_FLAGS ...)
但面试官更想看的是:你是否理解 CMake 在表达一个构建图(build graph)。
在这个图里:
- 节点是目标(target)
- 边是依赖关系(dependency)
- 每个目标带有自己的构建属性(头文件路径、宏、编译标准、链接需求)
现代 CMake 的设计方向,就是把这些属性和传播规则显式建模出来,而不是堆全局变量。
2. 为什么目标(target)是现代 CMake 的中心
C++ 工程中的编译问题,本质上几乎都可以落到某个目标上:
- 某个库需要哪些头文件目录
- 某个模块依赖哪些第三方库
- 某个可执行文件需要什么宏定义
- 某个目标要用 C++17 还是 C++20
因此现代 CMake 提倡:
- 先定义目标
- 再给目标挂属性
- 最后声明目标之间的依赖
典型写法:
add_library(core STATIC core.cpp)
target_include_directories(core PUBLIC include)
target_compile_features(core PUBLIC cxx_std_20)
target_compile_definitions(core PRIVATE CORE_USE_FAST_PATH=1)
add_executable(app main.cpp)
target_link_libraries(app PRIVATE core)
这个写法的好处是:
- 可读性强:一眼能看出谁依赖谁
- 边界清晰:属性不会无意污染别的模块
- 传播正确:接口属性能自动沿依赖传递
- 便于复用:库目标可单独拿出去给别的工程用
3. 什么叫“使用要求”(Usage Requirements)
这是面试里真正加分的关键词。
所谓使用要求,就是:
一个目标为了让别的目标正确使用它,需要向外暴露哪些构建信息。
例如一个库 core 对外暴露头文件 include/core/api.h,那依赖它的目标就必须知道 include/ 路径;
如果 core 的头文件里使用了 C++20 特性,那依赖它的目标也至少要支持 C++20;
如果头文件暴露了某个宏行为,那这个宏可能也要传出去。
这些都是使用要求。
现代 CMake 通过以下命令来表达使用要求:
target_include_directoriestarget_compile_definitionstarget_compile_optionstarget_compile_featurestarget_link_libraries
配合 PUBLIC / INTERFACE 实现传播。
这也是为什么说:
target_link_libraries() 不只是“链接库”,它还会传递目标的使用要求。
4. PUBLIC / PRIVATE / INTERFACE 的判断原则
面试里不要死背定义,最好给出判断标准:
判断方法
问自己两个问题:
- 当前目标自己编译时要不要用?
- 依赖当前目标的其他目标要不要知道?
然后套规则:
- 只自己用:
PRIVATE - 自己用,别人也要知道:
PUBLIC - 自己不用,别人要知道:
INTERFACE
典型场景
场景 A:头文件目录属于对外接口
target_include_directories(core PUBLIC include)
因为 core 自己编译可能会包含这些头文件,使用 core 的目标也要能找到这些头文件,所以是 PUBLIC。
场景 B:内部实现专用宏
target_compile_definitions(core PRIVATE CORE_DEBUG_IMPL)
这个宏只影响 core.cpp 的实现细节,不应该暴露给外部,所以用 PRIVATE。
场景 C:纯头文件库
add_library(my_header_only INTERFACE)
target_include_directories(my_header_only INTERFACE include)
因为没有源文件编译,所以只能是 INTERFACE。
5. 为什么不推荐全局写法
例如下面这种老式写法:
include_directories(include)
add_definitions(-DUSE_FAST_MODE)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O2")
问题在于:
1)污染范围不清晰
你很难知道这些设置影响了哪些目标。
2)可维护性差
工程大了以后,很多编译问题变成“这个宏到底是谁加的”。
3)复用性差
把一个模块单独拿出来时,它依赖的构建信息可能散落在各处,无法自描述。
4)不利于依赖传播
全局写法表达不了“哪些属性该向下游传播,哪些不该”。
所以现代工程里更推荐:
- 用
target_*家族命令 - 尽量避免全局状态
- 用目标表达依赖,而不是手动拼路径
6. 编译标准应该怎么设
面试中经常有人写:
set(CMAKE_CXX_STANDARD 20)
这不是错,但更现代、更稳的方式通常是:
target_compile_features(app PRIVATE cxx_std_20)
原因是:
- 它是目标级别的,不是全局的
- 更符合 target-based 思路
- 它描述的是“需要这个语言能力”,而不是直接硬编码某个编译器参数
如果工程统一标准、规模不大,CMAKE_CXX_STANDARD 也可以接受;
但面试里最好体现你知道:更推荐目标级别表达编译需求。
7. 第三方库接入的工程实践
现代 CMake 接第三方库时,一个重要原则是:
能链接“目标”,就不要手写 include 路径和
.lib/.a文件路径。
推荐顺序通常是:
方式一:find_package
适合系统已安装或包管理器提供的库。
find_package(spdlog REQUIRED)
target_link_libraries(app PRIVATE spdlog::spdlog)
优点:
- 规范
- 易维护
- 第三方库的使用要求通常会自动带过来
方式二:add_subdirectory
适合把依赖源码直接纳入工程。
add_subdirectory(extern/mylib)
target_link_libraries(app PRIVATE mylib)
优点:
- 本地开发方便
- 依赖和主工程一起构建
方式三:FetchContent
适合自动拉取依赖源码。
工程里常用,但面试里说清思路即可,不必展开太细。
8. 一个较完整的现代 CMake 示例
cmake_minimum_required(VERSION 3.20)
project(Demo LANGUAGES CXX)
add_library(core STATIC
src/core.cpp
)
target_include_directories(core
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/include
)
target_compile_features(core
PUBLIC
cxx_std_20
)
target_compile_definitions(core
PRIVATE
CORE_ENABLE_LOG=1
)
find_package(fmt REQUIRED)
target_link_libraries(core
PUBLIC
fmt::fmt
)
add_executable(app
src/main.cpp
)
target_link_libraries(app
PRIVATE
core
)
面试时可以这样解释:
core是可复用库模块- 头文件目录是对外接口,因此
PUBLIC - 编译标准是对外要求,因此
PUBLIC - 内部日志宏只影响实现,因此
PRIVATE core依赖fmt,如果core的头文件或接口中暴露了fmt相关能力,可以用PUBLIC;否则更稳妥的选择通常是PRIVATEapp只依赖core,通过依赖传播拿到需要的构建信息
这里还能顺便体现一个面试很加分的点:
PUBLIC不是越多越好,应该只暴露真正属于接口的一部分;能PRIVATE就尽量PRIVATE,降低耦合。
对比总结
| 对比项 | 是什么 | 适用场景 | 优点 | 风险 / 缺点 | 面试回答重点 |
|---|---|---|---|---|---|
| CMake | 构建系统生成器 | 跨平台 C++ 工程 | 统一描述工程,生成不同平台构建文件 | 不是直接编译工具,初学者容易混淆 | 强调“描述工程 + 生成构建系统” |
| Make / Ninja | 构建执行工具 | 执行编译任务 | 构建效率高 | 不负责高层工程描述 | 说清与 CMake 的分工 |
| 编译器(g++/clang++/MSVC) | 真正编译/链接代码 | 编译单个或多个翻译单元 | 最终产出二进制 | 不负责完整工程组织 | 不要把 CMake 说成编译器 |
include_directories() | 全局添加头文件目录 | 老项目、简单项目 | 写起来快 | 污染全局,边界不清晰 | 面试里明确说不推荐大量使用 |
target_include_directories() | 给目标设置头文件目录 | 现代 CMake | 作用域清晰、可传播 | 需要理解 target 和可见性 | 推荐写法,体现工程意识 |
set(CMAKE_CXX_STANDARD ...) | 全局设置 C++ 标准 | 小型统一工程 | 简单直接 | 粒度粗,不够模块化 | 知道可用,但更推荐 target 级别表达 |
target_compile_features(... cxx_std_20) | 目标级描述语言能力需求 | 现代工程 | 更符合 target-based 思路 | 需要理解依赖传播 | 更适合在面试中作为推荐方案 |
PRIVATE | 仅当前目标使用 | 内部实现选项、内部宏、私有链接依赖 | 封装性好 | 设错了会导致下游缺信息 | 默认优先考虑 PRIVATE |
PUBLIC | 当前目标使用且向下游传播 | 对外接口头文件目录、接口依赖、接口标准要求 | 传播自然 | 用滥了会扩大耦合 | 只有属于接口的内容才设 PUBLIC |
INTERFACE | 当前目标自己不用,仅传给下游 | 纯头文件库、接口库 | 很适合表达“只传递不编译” | 初学者容易误解 | 面试里说出 header-only 场景会加分 |
find_package() | 查找外部已安装依赖 | 系统库/包管理器依赖 | 规范、可维护 | 依赖包配置质量 | 优先通过目标接入第三方库 |
add_subdirectory() | 将依赖源码纳入工程 | 自带源码依赖 | 简单直观 | 工程耦合更高 | 适合源码随工程构建的场景 |
易错点
- 把 CMake 说成“编译器”
- 把 CMake 和 Make / Ninja 混为一谈
- 只会背命令,不理解“目标 + 依赖 + 使用要求”
- 继续使用全局
include_directories()、add_definitions()、CMAKE_CXX_FLAGS - 说不清
PUBLIC/PRIVATE/INTERFACE - 不知道
target_link_libraries()还会带来使用要求传播 - 把所有东西都写成
PUBLIC,导致接口膨胀 - 不区分“实现依赖”和“接口依赖”
- 不理解纯头文件库为什么适合
INTERFACE - 只会写单文件 demo,不会表达多模块工程结构
- 把第三方库接入理解成“手动写 include 路径 + 手动写库文件路径”,而不是目标化接入
记忆技巧
-
记住现代 CMake 三步:
- 先建目标
- 再挂属性
- 最后连依赖
-
记
PUBLIC / PRIVATE / INTERFACE的口诀: 自己用不用,别人知不知道 -
记 target-based 的一句话: 构建信息不要全局乱撒,要绑定到目标上
-
记现代 CMake 的核心目标: 让依赖关系和使用要求显式、可传播、可维护
-
记面试高频判断原则: 属于接口的,才考虑
PUBLIC;只影响实现的,优先PRIVATE
面试速答版
CMake 不是编译器,而是构建系统生成器。它用来描述 C++ 工程中的目标、依赖、头文件目录、编译选项和链接关系,再生成 Makefile、Ninja 或 VS 工程,由底层构建工具调用编译器完成构建。
现代 CMake 的核心思想是 target-based。也就是先定义目标,再把 include、宏、编译特性、链接关系挂到目标上,而不是用全局配置污染整个工程。
PRIVATE 表示只当前目标使用,PUBLIC 表示当前目标使用并向依赖方传播,INTERFACE 表示当前目标自己不用,只传给依赖方。
所以面试考 CMake,本质上是在看你是否理解工程里的目标、依赖和使用要求传播。
面试加分版
如果让我概括 CMake,我会说它不是编译器,而是一个跨平台的构建描述工具。它的职责不是直接编译代码,而是把工程里的目标、依赖关系、编译标准、头文件目录、宏定义和链接关系组织起来,再生成具体平台下的构建系统,比如 Makefile、Ninja 或 Visual Studio 工程,最后再由底层工具去调用 g++、clang++ 或 MSVC 完成编译。
现代 CMake 的重点是 以目标为中心。在真实工程里,编译选项、include 路径、宏定义都不应该全局乱撒,而应该绑定到具体目标上。比如一个 core 库对外暴露头文件目录,那这个目录应该挂在 core 上;一个 app 依赖 core,就通过 target_link_libraries(app PRIVATE core) 建立依赖。这样 core 的使用要求可以自动传播到 app,构建边界更清晰,也更利于维护大型工程。
PUBLIC、PRIVATE、INTERFACE 是面试很常追问的点。我的理解不是死记定义,而是看两个问题:当前目标自己要不要用,依赖它的目标要不要知道。只自己用就是 PRIVATE;自己用、别人也要知道就是 PUBLIC;自己不用、但别人要知道就是 INTERFACE。比如头文件目录通常是接口的一部分,常用 PUBLIC;内部实现宏一般用 PRIVATE;纯头文件库适合 INTERFACE。
工程实践里我会优先选择现代 CMake 的 target-based 写法,并尽量通过目标来接入第三方库,比如 find_package(fmt REQUIRED) 后链接 fmt::fmt,而不是手工写一堆 include 和库路径。因为目标方式更清晰,传播规则也更稳定。
所以我觉得面试问 CMake,本质上不是看你会不会背命令,而是看你有没有把 C++ 工程当成一个可维护的依赖系统来理解。