⚡C++ 编译链接与构建

CMake 基础(现代 C++ 工程面试笔记)

面试回答

常见问法

  • CMake 是干什么的?
  • CMake 和 Make / Ninja / 编译器是什么关系?
  • 为什么现代 C++ 工程面试经常会问 CMake?
  • 什么是 target-based CMake?
  • target_include_directoriestarget_link_librariestarget_compile_definitions 为什么都强调“挂到目标上”?
  • PUBLICPRIVATEINTERFACE 分别是什么意思?
  • 第三方库在 CMake 里通常怎么接入?
  • 为什么不推荐 include_directories()link_libraries() 这种全局写法?

回答

CMake 不是编译器,也不是直接执行编译的工具,它本质上是一个构建系统生成器。 它负责描述工程里的:

  • 有哪些构建目标(可执行文件、静态库、动态库、接口库)
  • 目标之间的依赖关系
  • 头文件搜索路径
  • 编译选项、宏定义、语言标准
  • 链接关系
  • 第三方库接入方式

然后根据平台和生成器,生成对应的构建文件,比如 MakefilesNinja、Visual Studio 工程等,再由底层构建工具真正去调用编译器完成编译。

现代 C++ 面试问 CMake,重点通常不在背命令,而在看你有没有工程意识。核心看三点:

  1. 你是否理解“目标”是构建的基本单位
  2. 你是否会正确表达依赖和使用要求(usage requirements)
  3. 你是否知道 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)PUBLICPRIVATEINTERFACE 怎么理解?

这是 CMake 里最常被追问的点,本质是在描述**“当前目标自己要不要用,以及依赖它的目标要不要继承”**。

  • PRIVATE:只给当前目标自己用,不向外传播
  • PUBLIC:当前目标自己用,依赖它的目标也要继承
  • INTERFACE:当前目标自己不用,但依赖它的目标要继承

例如:

add_library(core STATIC core.cpp)
target_include_directories(core PUBLIC include)
target_compile_definitions(core PRIVATE CORE_ENABLE_LOG)

解释:

  • includecore 对外接口的一部分,所以用 PUBLIC
  • CORE_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 里优先用“目标”方式接入第三方库,而不是手写裸路径。

常见方式有:

  1. find_package() 找已安装包
  2. add_subdirectory() 引入源码依赖
  3. FetchContent 拉取源码依赖

例如:

find_package(fmt REQUIRED)

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

这里最重要的不是记住 fmt,而是要体现一个思想:

第三方库也最好以“目标”的形式使用,因为它会自带 include、链接信息、编译要求,接入更稳。


原理展开

1. CMake 的核心不是“命令”,而是“构建模型”

很多人初学 CMake 时容易把注意力放在命令本身,比如背:

  • add_executable
  • include_directories
  • link_directories
  • set(CMAKE_CXX_FLAGS ...)

但面试官更想看的是:你是否理解 CMake 在表达一个构建图(build graph)

在这个图里:

  • 节点是目标(target)
  • 边是依赖关系(dependency)
  • 每个目标带有自己的构建属性(头文件路径、宏、编译标准、链接需求)

现代 CMake 的设计方向,就是把这些属性和传播规则显式建模出来,而不是堆全局变量。


2. 为什么目标(target)是现代 CMake 的中心

C++ 工程中的编译问题,本质上几乎都可以落到某个目标上:

  • 某个库需要哪些头文件目录
  • 某个模块依赖哪些第三方库
  • 某个可执行文件需要什么宏定义
  • 某个目标要用 C++17 还是 C++20

因此现代 CMake 提倡:

  1. 先定义目标
  2. 再给目标挂属性
  3. 最后声明目标之间的依赖

典型写法:

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_directories
  • target_compile_definitions
  • target_compile_options
  • target_compile_features
  • target_link_libraries

配合 PUBLIC / INTERFACE 实现传播。

这也是为什么说: target_link_libraries() 不只是“链接库”,它还会传递目标的使用要求。


4. PUBLIC / PRIVATE / INTERFACE 的判断原则

面试里不要死背定义,最好给出判断标准:

判断方法

问自己两个问题:

  1. 当前目标自己编译时要不要用?
  2. 依赖当前目标的其他目标要不要知道?

然后套规则:

  • 只自己用: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;否则更稳妥的选择通常是 PRIVATE
  • app 只依赖 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 三步:

    1. 先建目标
    2. 再挂属性
    3. 最后连依赖
  • 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,构建边界更清晰,也更利于维护大型工程。

PUBLICPRIVATEINTERFACE 是面试很常追问的点。我的理解不是死记定义,而是看两个问题:当前目标自己要不要用,依赖它的目标要不要知道。只自己用就是 PRIVATE;自己用、别人也要知道就是 PUBLIC;自己不用、但别人要知道就是 INTERFACE。比如头文件目录通常是接口的一部分,常用 PUBLIC;内部实现宏一般用 PRIVATE;纯头文件库适合 INTERFACE

工程实践里我会优先选择现代 CMake 的 target-based 写法,并尽量通过目标来接入第三方库,比如 find_package(fmt REQUIRED) 后链接 fmt::fmt,而不是手工写一堆 include 和库路径。因为目标方式更清晰,传播规则也更稳定。 所以我觉得面试问 CMake,本质上不是看你会不会背命令,而是看你有没有把 C++ 工程当成一个可维护的依赖系统来理解。