CMake学习篇二:CMake 核心概念
- 笔记
- 8小时前
- 17热度
- 0评论
- ✅ 已完成 引入篇:从 GCC 到 Makefile 再到 CMake:理解为什么需要 CMake
- ✅ 已完成 篇一:快速一步一步入门到进阶:会创建基本的 CMake 项目
- 📚 本篇目标:深入理解 CMake 的核心概念和设计思想
1. CMakeLists.txt
1.1 为什么需要 CMakeLists.txt?
在 Makefile 中,你需要手写每个文件的编译规则:
# Makefile 方式:繁琐
main.o: main.c
gcc -c main.c -o main.o
add.o: add.c
gcc -c add.c -o add.o
CMakeLists.txt 只需要告诉 CMake"我要编译什么",CMake 自动生成规则:
# CMake 方式:简洁
add_executable(main main.c add.c)
1.2 基本结构
# 1. 版本要求(确保 CMake 功能可用)
cmake_minimum_required(VERSION 3.10)
# 2. 项目定义(设置项目名称、版本、语言)
project(ProjectName VERSION 1.0.0 LANGUAGES CXX)
# 3. 编译选项(设置 C++ 标准等)
set(CMAKE_CXX_STANDARD 17)
# 4. 添加目标(可执行文件或库)
add_executable(app main.cpp)
# 5. 链接库(如果需要)
target_link_libraries(app PRIVATE some_library)
1.3 CMakeLists.txt 的执行流程
1. cmake .. (配置阶段)
↓
2. 读取 CMakeLists.txt
↓
3. 解析指令,设置变量
↓
4. 生成构建系统文件(Makefile/Ninja/VS项目)
↓
5. cmake --build . (构建阶段)
↓
6. 调用底层构建工具(make/ninja/msbuild)
↓
7. 编译源文件,生成目标
- 配置阶段(
cmake ..):读取 CMakeLists.txt,生成构建文件 - 构建阶段(
cmake --build .):调用编译器,实际编译代码 - CMakeLists.txt 中的指令在配置阶段执行,不是在编译时执行
2. Target(目标)
2.1 为什么需要 Target?
在传统的 Makefile 中,你需要手动管理每个文件的编译规则和依赖关系:
# Makefile:手动管理依赖
libA.o: libA.c libA.h
gcc -c libA.c -I./include
libB.o: libB.c libB.h libA.h
gcc -c libB.c -I./include -I./libA
app: main.c libA.o libB.o
gcc main.c libA.o libB.o -o app
这种方式的问题:
- 依赖关系不清晰,容易遗漏
- 编译选项分散,难以维护
- 无法自动传递依赖信息
CMake 的 Target 是一个"智能对象",它通过属性(Properties)封装了构建所需的所有信息:
- 需要哪些源文件
- 依赖哪些库
- 头文件在哪里
- 需要什么编译选项
- 需要定义哪些宏
创建 Target 后,我们通过 target_xxx 命令为它配置这些属性。这样 CMake 就能自动处理复杂的依赖关系,让构建配置更清晰、更易维护。
2.2 创建 Target
Target 可以是四种类型:
# 1. 可执行文件
add_executable(my_app main.cpp utils.cpp)
# 2. 静态库(.a 或 .lib)
add_library(my_static_lib STATIC lib.cpp)
# 3. 动态库(.so 或 .dll)
add_library(my_shared_lib SHARED lib.cpp)
# 4. 接口库(仅头文件,无编译产物)
add_library(my_header_lib INTERFACE)
- 纯头文件库(header-only library)
- 仅传递编译选项或依赖关系
- 组织和管理一组相关的配置
2.3 配置 Target 的属性
创建 Target 后,需要为它配置各种属性。现代 CMake 使用 target_xxx 命令来配置:
add_library(mylib src/lib.cpp)
# 1. 设置头文件搜索路径
target_include_directories(mylib PUBLIC include/)
# 2. 链接其他库
target_link_libraries(mylib PUBLIC other_lib)
# 3. 设置编译选项
target_compile_options(mylib PRIVATE -Wall -Wextra)
# 4. 设置宏定义
target_compile_definitions(mylib PUBLIC VERSION=1.0)
# 5. 设置 C++ 标准(CMake 3.8+)
target_compile_features(mylib PUBLIC cxx_std_17)
CMake 为每个 Target 维护一套独立的属性:
| 属性类型 | 存储内容 | 设置命令 |
|---|---|---|
| INCLUDE_DIRECTORIES | 头文件搜索路径 | target_include_directories |
| LINK_LIBRARIES | 链接的库 | target_link_libraries |
| COMPILE_OPTIONS | 编译选项(如 -Wall) | target_compile_options |
| COMPILE_DEFINITIONS | 宏定义(如 DEBUG=1) | target_compile_definitions |
| COMPILE_FEATURES | 语言特性(如 C++17) | target_compile_features |
2.4 Target 之间的依赖关系
Target 可以依赖其他 Target,形成依赖链:
# CMakeLists.txt
# 创建库 A
add_library(libA src/a.cpp)
target_include_directories(libA PUBLIC include/a)
# 创建库 B,依赖库 A
add_library(libB src/b.cpp)
target_include_directories(libB PUBLIC include/b)
target_link_libraries(libB PUBLIC libA) # B 依赖 A
# 创建可执行文件,依赖库 B
add_executable(app main.cpp)
target_link_libraries(app PRIVATE libB) # app 依赖 B
# 依赖链:app → libB → libA
# app 会自动获得 libA 和 libB 的 PUBLIC 属性
依赖传递的魔法:
- app 链接 libB 时,自动获得 libB 的 PUBLIC 属性
- libB 的 PUBLIC 属性包括它依赖的 libA 的 PUBLIC 属性
- 因此 app 无需显式链接 libA,也能访问 libA 的公共头文件
- 自动依赖传递:链接 B 时自动获得 A 的 PUBLIC 属性
- 清晰的依赖关系:一目了然谁依赖谁
- 模块化:每个 Target 独立配置,互不干扰
- 类型安全:CMake 会检查 Target 是否存在
2.5 作用域:PRIVATE vs PUBLIC vs INTERFACE
在上面的例子中,你可能注意到了 PUBLIC 和 PRIVATE 关键字。为什么需要它们?
2.5.1 为什么需要作用域?
问题场景:
# CMakeLists.txt
add_library(mylib src/lib.cpp src/internal.cpp)
# 这个库有两类头文件:
# 1. include/mylib.h - 公共 API,用户需要
# 2. src/internal.h - 内部实现,用户不需要
# 如果不区分作用域:
target_include_directories(mylib PUBLIC include/ src/)
# 问题:链接 mylib 的用户也能看到 src/internal.h
# 这暴露了内部实现细节,破坏了封装性
解决方案:使用作用域关键字
# CMakeLists.txt
target_include_directories(mylib
PUBLIC include/ # 公共接口,依赖者需要
PRIVATE src/ # 内部实现,仅自己用
)
这样:
- mylib 自己可以访问
include/和src/ - 链接 mylib 的用户只能访问
include/,看不到src/
2.5.2 三种作用域的本质
| 关键字 | 当前 Target 使用 | 依赖者继承 | 典型场景 |
|---|---|---|---|
| PRIVATE | ✅ 是 | ❌ 否 | 内部实现细节 |
| PUBLIC | ✅ 是 | ✅ 是 | 对外接口的一部分 |
| INTERFACE | ❌ 否 | ✅ 是 | 仅用于接口(如头文件库) |
形象比喻:
把 Target 想象成一个公司:
- PRIVATE:公司内部的管理制度和流程(员工知道,外部客户不知道)
- PUBLIC:公司的产品和服务标准(员工要遵守,客户也能看到)
- INTERFACE:公司对外的服务承诺(仅客户需要知道,内部可能不同实现)
2.5.3 target_include_directories 的底层原理
# CMakeLists.txt
target_include_directories(mylib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
作用:给 mylib 这个 Target 添加头文件搜索路径(-I 路径),告诉编译器去哪里找头文件。
底层发生了什么?
- CMake 在内部为
mylib这个 Target 维护一个属性列表 - 把路径记录到
mylibTarget 的INCLUDE_DIRECTORIES属性中 - 根据你写的是 PUBLIC、PRIVATE 还是 INTERFACE,决定这个路径是否要传播给依赖它的其他 Target
三种可见性的底层效果:
| 可见性 | 含义 | 底层效果 |
|---|---|---|
| PRIVATE | 只有自己用 | 只加到 mylib 自己的编译命令中 |
| PUBLIC | 自己和别人都要用 | 加到 mylib 自己 + 所有链接 mylib 的 Target |
| INTERFACE | 只有别人用(接口库常用) | 只传播给链接它的 Target,自己不使用 |
实际编译命令:
# CMakeLists.txt
add_library(mylib src/lib.cpp)
target_include_directories(mylib
PUBLIC include/
PRIVATE src/internal/
)
add_executable(app main.cpp)
target_link_libraries(app PRIVATE mylib)
生成的编译命令:
# 编译 mylib
g++ -c src/lib.cpp -I include/ -I src/internal/ -o lib.o
# 编译 app
g++ -c main.cpp -I include/ -o main.o # 只有 PUBLIC 的 include/
2.5.4 target_link_libraries 的底层原理
# libB/CMakeLists.txt
target_link_libraries(libB PUBLIC libA) # B 依赖 A
# app/CMakeLists.txt
target_link_libraries(app PRIVATE libB) # app 依赖 B
作用:
- 建立链接关系:告诉 CMake 这个 Target 在链接阶段需要哪些库(生成
-l参数) - 建立依赖关系:让 CMake 知道构建顺序(先构建被依赖的库)
- 传播接口属性:根据 PUBLIC/PRIVATE/INTERFACE,决定是否把被依赖库的 PUBLIC 属性(头文件路径、编译定义等)传递给当前 Target
底层发生了什么?
CMake 会做三件事:
- 链接关系记录:把
libA加入libBTarget 的LINK_LIBRARIES属性 - 传递头文件和编译选项:如果
libA有PUBLIC的include_directories或compile_definitions,会自动传递给libB - 依赖顺序控制:CMake 会根据依赖关系自动计算正确的链接顺序(拓扑排序),避免链接错误
属性传播图解:
库 A 的属性:
┌─────────────────────────────────┐
│ PRIVATE: src/a/internal │ ← 仅 A 自己用
├─────────────────────────────────┤
│ PUBLIC: include/a │ ← A 用,依赖 A 的也用
├─────────────────────────────────┤
│ INTERFACE: interface/a │ ← A 不用,仅依赖 A 的用
└─────────────────────────────────┘
↓ target_link_libraries(B PUBLIC A)
库 B 继承的属性:
┌─────────────────────────────────┐
│ PUBLIC: include/a │ ✅ 继承
│ INTERFACE: interface/a │ ✅ 继承
│ PRIVATE: src/a/internal │ ❌ 不继承
└─────────────────────────────────┘
↓ target_link_libraries(app PRIVATE B)
app 继承的属性:
┌─────────────────────────────────┐
│ B 的 PUBLIC + INTERFACE │ ✅ 继承
│ A 的 PUBLIC + INTERFACE │ ✅ 继承(因为 B PUBLIC A)
└─────────────────────────────────┘
2.5.5 target_link_libraries 的作用域
target_link_libraries 本身也有作用域,控制依赖关系的传播:
# CMakeLists.txt
# 库 A
add_library(libA a.cpp)
# 库 B 依赖 A(PRIVATE)
add_library(libB b.cpp)
target_link_libraries(libB PRIVATE libA)
# 库 C 依赖 A(PUBLIC)
add_library(libC c.cpp)
target_link_libraries(libC PUBLIC libA)
# 应用程序
add_executable(app main.cpp)
target_link_libraries(app PRIVATE libB libC)
结果分析:
| Target | 链接的库 | 原因 |
|---|---|---|
| libB | libA | 直接依赖 |
| libC | libA | 直接依赖 |
| app | libB, libC, libA | libC 的 PUBLIC 传递了 libA |
关键点:
libB PRIVATE libA:libA 是 libB 的内部实现,不传递给 applibC PUBLIC libA:libA 是 libC 的公共接口,传递给 app- app 自动链接 libA,无需显式声明
2.5.6 如何选择作用域?
决策流程图:
这个属性(头文件/宏/库)在公共头文件中使用吗?
├─ 是 → 用 PUBLIC
│ 例:公共头文件 #include <boost/shared_ptr.hpp>
│
└─ 否 → 当前 Target 需要使用吗?
├─ 是 → 用 PRIVATE
│ 例:内部实现文件用的头文件
│
└─ 否 → 用 INTERFACE
例:仅头文件库(header-only library)
常见场景速查表:
| 场景 | 作用域 | 示例 |
|---|---|---|
| 公共 API 头文件路径 | PUBLIC | target_include_directories(lib PUBLIC include/) |
| 内部实现头文件路径 | PRIVATE | target_include_directories(lib PRIVATE src/) |
| 公共头文件中用到的第三方库 | PUBLIC | target_link_libraries(lib PUBLIC Boost::asio) |
| 仅 .cpp 中用到的第三方库 | PRIVATE | target_link_libraries(lib PRIVATE fmt::fmt) |
| 公共 API 的宏定义 | PUBLIC | target_compile_definitions(lib PUBLIC API_VERSION=2) |
| 内部调试宏 | PRIVATE | target_compile_definitions(lib PRIVATE DEBUG_LOG) |
| 纯头文件库 | INTERFACE | target_include_directories(header_lib INTERFACE include/) |
| 编译选项(警告等) | PRIVATE | target_compile_options(lib PRIVATE -Wall) |
2.5.7 实际应用场景
场景 1:第三方库依赖
# CMakeLists.txt
# 场景:你的库在公共头文件中使用了 Boost
add_library(mylib src/lib.cpp)
# ❌ 错误做法:PRIVATE
target_link_libraries(mylib PRIVATE Boost::filesystem)
# 问题:用户代码 #include "mylib.h" 时,找不到 Boost 头文件
# ✅ 正确做法:PUBLIC
target_link_libraries(mylib PUBLIC Boost::filesystem)
# 结果:用户自动获得 Boost 的头文件路径和链接库
判断标准:
你的公共头文件中是否 #include <boost/filesystem.hpp>?
├─ 是 → PUBLIC(用户也需要 Boost)
└─ 否 → PRIVATE(仅内部实现用)
场景 2:纯头文件库(Header-Only Library)
# CMakeLists.txt
# 纯头文件库:没有 .cpp 文件
add_library(header_only_lib INTERFACE)
target_include_directories(header_only_lib INTERFACE include/)
target_compile_features(header_only_lib INTERFACE cxx_std_17)
# 用户代码
add_executable(app main.cpp)
target_link_libraries(app PRIVATE header_only_lib)
# app 自动获得:
# - include/ 路径
# - C++17 标准要求
为什么用 INTERFACE?
- 头文件库自己没有编译过程(没有 .cpp)
- 所有配置都是给依赖者用的
2.5.8 常见错误与调试
错误 1:头文件找不到
# CMakeLists.txt
# ❌ 错误:用了 PRIVATE,但用户需要这个头文件
add_library(mylib lib.cpp)
target_include_directories(mylib PRIVATE include/)
# 用户代码
add_executable(app main.cpp)
target_link_libraries(app PRIVATE mylib)
# 编译错误:fatal error: mylib.h: No such file or directory
解决方案:改为 PUBLIC
# CMakeLists.txt
target_include_directories(mylib PUBLIC include/)
错误 2:过度传播依赖
# CMakeLists.txt
# ❌ 错误:用了 PUBLIC,但用户不需要这个库
add_library(mylib lib.cpp)
target_link_libraries(mylib PUBLIC fmt::fmt) # fmt 仅在 .cpp 中使用
# 问题:所有链接 mylib 的用户都被迫依赖 fmt
解决方案:改为 PRIVATE
# CMakeLists.txt
target_link_libraries(mylib PRIVATE fmt::fmt)
调试技巧:
# 查看 Target 的属性
get_target_property(INCLUDES mylib INTERFACE_INCLUDE_DIRECTORIES)
message("mylib PUBLIC includes: ${INCLUDES}")
get_target_property(LINKS mylib INTERFACE_LINK_LIBRARIES)
message("mylib PUBLIC links: ${LINKS}")
2.6 旧方式 vs 现代方式
旧方式:全局命令(不推荐)
# 影响所有后续定义的 Target
include_directories(include/)
add_definitions(-DDEBUG)
add_compile_options(-Wall)
add_library(lib1 lib1.cpp) # 继承上面所有设置
add_library(lib2 lib2.cpp) # 也继承上面所有设置
问题:
- 全局污染,所有 Target 都受影响
- 无法精确控制每个 Target 的配置
- 依赖关系不清晰
- 不支持作用域控制
现代方式:Target-based 命令(推荐)
add_library(lib1 lib1.cpp)
target_include_directories(lib1 PRIVATE include/)
target_compile_definitions(lib1 PRIVATE DEBUG)
target_compile_options(lib1 PRIVATE -Wall)
add_library(lib2 lib2.cpp)
# lib2 不受 lib1 配置的影响
优势对比:
| 维度 | 旧方式 | 现代方式 |
|---|---|---|
| 作用范围 | 全局,影响所有后续 Target | 只作用于指定 Target |
| 依赖关系 | 看不出来 | 明确、清晰、可追踪 |
| 传播控制 | 无法精细控制 | 通过 PUBLIC/PRIVATE/INTERFACE 精确控制 |
| 底层存储 | 全局变量 | 每个 Target 自己的属性 |
| 适合大型项目 | 非常差 | 极好 |
| 封装性 | 差 | 好 |
3. 变量系统
3.1 为什么需要变量?
变量让你的 CMakeLists.txt 更灵活、更易维护:
- 避免重复写相同的路径或值
- 方便修改配置(改一处,全局生效)
- 支持条件编译(根据平台或配置选择不同的值)
3.2 定义和使用变量
# 定义普通变量
set(MY_VAR "value")
set(MY_LIST item1 item2 item3)
# 使用变量
message("Variable value: ${MY_VAR}")
# 列表操作
list(APPEND MY_LIST item4)
list(LENGTH MY_LIST list_length)
3.3 变量的类型
普通变量
set(MY_VAR "hello")
set(MY_NUMBER 42)
set(MY_PATH /usr/local/include)
列表变量
set(SOURCES main.cpp utils.cpp helper.cpp)
# 等价于
set(SOURCES "main.cpp;utils.cpp;helper.cpp")
# 使用列表
add_executable(app ${SOURCES})
缓存变量(可在命令行修改)
set(MY_OPTION "default" CACHE STRING "Description")
# 命令行修改:cmake .. -DMY_OPTION=new_value
环境变量
# 读取环境变量
message("PATH: $ENV{PATH}")
# 设置环境变量
set(ENV{MY_VAR} "value")
3.4 常用内置变量
${CMAKE_SOURCE_DIR} # 源代码根目录
${CMAKE_BINARY_DIR} # 构建目录
${CMAKE_CURRENT_SOURCE_DIR} # 当前 CMakeLists.txt 所在目录
${CMAKE_CURRENT_BINARY_DIR} # 当前构建目录
${PROJECT_NAME} # 项目名称
${PROJECT_VERSION} # 项目版本
${CMAKE_CXX_COMPILER} # C++ 编译器路径
${CMAKE_BUILD_TYPE} # 构建类型(Debug/Release)
3.5 变量的作用域
# 全局作用域
set(GLOBAL_VAR "global")
function(my_function)
# 函数作用域(独立)
set(LOCAL_VAR "local")
message("Inside function: ${GLOBAL_VAR}") # 可以访问全局变量
endfunction()
my_function()
message("Outside function: ${LOCAL_VAR}") # 空值,访问不到函数内的变量
4. 子目录与头文件管理
4.1 add_subdirectory:引入子目录
4.1.1 基本用法
# 根目录 CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(MyProject)
# 引入子目录
add_subdirectory(src)
add_subdirectory(lib)
add_subdirectory(tests)
4.1.2 执行流程
项目结构:
MyProject/
├── CMakeLists.txt # 根 CMakeLists.txt
├── src/
│ ├── CMakeLists.txt # src 的 CMakeLists.txt
│ └── main.cpp
└── lib/
├── CMakeLists.txt # lib 的 CMakeLists.txt
├── mylib.cpp
└── mylib.h
执行流程:
1. cmake .. 开始配置
↓
2. 读取根目录 CMakeLists.txt
↓
3. 遇到 add_subdirectory(src)
↓
4. 进入 src/ 目录,读取 src/CMakeLists.txt
↓
5. 执行 src/CMakeLists.txt 中的所有指令
↓
6. 返回根目录,继续执行
↓
7. 遇到 add_subdirectory(lib)
↓
8. 进入 lib/ 目录,读取 lib/CMakeLists.txt
↓
9. 执行完毕,返回根目录
↓
10. 根目录 CMakeLists.txt 执行完毕
add_subdirectory会立即进入子目录并执行子目录的 CMakeLists.txt- 子目录中定义的 Target 在父目录中可见
- 子目录继承父目录的变量,但修改不会影响父目录(除非用
PARENT_SCOPE)
4.1.3 实际例子
# 根目录 CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(MyApp)
set(CMAKE_CXX_STANDARD 17)
# 引入库目录
add_subdirectory(lib)
# 引入源码目录
add_subdirectory(src)
# lib/CMakeLists.txt
add_library(mylib mylib.cpp)
target_include_directories(mylib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
# src/CMakeLists.txt
add_executable(app main.cpp)
# 可以直接使用 lib 目录中定义的 mylib 目标
target_link_libraries(app PRIVATE mylib)
4.1.4 变量作用域
# 根目录 CMakeLists.txt
set(MY_VAR "root value")
add_subdirectory(sub)
message("After subdirectory: ${MY_VAR}") # 仍然是 "root value"
# sub/CMakeLists.txt
message("In subdirectory: ${MY_VAR}") # "root value"(继承)
set(MY_VAR "sub value") # 修改局部副本
message("Modified: ${MY_VAR}") # "sub value"
# 如果要修改父目录的变量:
set(MY_VAR "sub value" PARENT_SCOPE) # 影响父目录
4.2 头文件路径管理
4.2.1 include_directories(旧式,不推荐)
# ❌ 旧式方法:全局影响所有目标
include_directories(include/)
include_directories(third_party/include/)
add_executable(app1 main1.cpp) # 自动包含上面的路径
add_executable(app2 main2.cpp) # 也自动包含上面的路径
问题:
- 影响当前目录及所有子目录的所有目标
- 无法控制作用域(PRIVATE/PUBLIC/INTERFACE)
- 容易造成头文件污染
- 不符合现代 CMake 的 Target-based 设计
4.2.2 target_include_directories(现代方法,推荐)
# ✅ 现代方法:针对特定目标
add_library(mylib src/lib.cpp)
target_include_directories(mylib
PUBLIC include/ # 对外接口
PRIVATE src/internal/ # 内部实现
)
add_executable(app main.cpp)
target_link_libraries(app PRIVATE mylib)
# app 自动获得 mylib 的 PUBLIC 头文件路径
优势:
- 精确控制每个目标的头文件路径
- 支持 PRIVATE/PUBLIC/INTERFACE 作用域
- 依赖传递清晰
- 符合现代 CMake 最佳实践
4.2.3 对比示例
# ❌ 旧式:全局污染
include_directories(boost/include)
include_directories(opencv/include)
add_library(lib1 lib1.cpp) # 包含 boost 和 opencv
add_library(lib2 lib2.cpp) # 也包含 boost 和 opencv(即使不需要)
add_executable(app main.cpp) # 也包含所有路径
# ✅ 现代:精确控制
add_library(lib1 lib1.cpp)
target_include_directories(lib1 PRIVATE boost/include) # 仅 lib1 用
add_library(lib2 lib2.cpp)
target_include_directories(lib2 PRIVATE opencv/include) # 仅 lib2 用
add_executable(app main.cpp)
target_link_libraries(app PRIVATE lib1 lib2)
# app 不会获得 lib1 和 lib2 的 PRIVATE 头文件路径
4.3 完整的多目录项目示例
项目结构
MyProject/
├── CMakeLists.txt # 根配置
├── include/ # 公共头文件
│ └── myproject/
│ └── api.h
├── src/ # 源码目录
│ ├── CMakeLists.txt
│ ├── main.cpp
│ └── internal.h
├── lib/ # 库目录
│ ├── CMakeLists.txt
│ ├── mylib.cpp
│ └── mylib.h
└── third_party/ # 第三方库
└── json/
└── json.hpp
根目录 CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(MyProject VERSION 1.0.0)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 设置输出目录
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib)
# 引入子目录(顺序很重要!)
add_subdirectory(lib) # 先构建库
add_subdirectory(src) # 再构建可执行文件(依赖库)
lib/CMakeLists.txt
# 创建库
add_library(mylib mylib.cpp)
# 设置头文件路径
target_include_directories(mylib
PUBLIC
${CMAKE_SOURCE_DIR}/include # 公共 API 头文件
${CMAKE_CURRENT_SOURCE_DIR} # 库自己的头文件
PRIVATE
${CMAKE_SOURCE_DIR}/third_party # 第三方库(内部使用)
)
# 设置编译选项
target_compile_options(mylib PRIVATE -Wall -Wextra)
# 设置宏定义
target_compile_definitions(mylib
PUBLIC MYLIB_VERSION="${PROJECT_VERSION}"
PRIVATE MYLIB_INTERNAL_DEBUG
)
src/CMakeLists.txt
# 创建可执行文件
add_executable(app main.cpp)
# 链接库
target_link_libraries(app PRIVATE mylib)
# app 自动继承 mylib 的 PUBLIC 属性:
# - include/ 头文件路径
# - mylib.h 所在路径
# - MYLIB_VERSION 宏定义
# 但看不到 mylib 的 PRIVATE 属性:
# - third_party/ 路径
# - MYLIB_INTERNAL_DEBUG 宏
4.4 常见问题与最佳实践
问题 1:头文件找不到
# ❌ 错误:使用相对路径
target_include_directories(mylib PUBLIC include/)
# ✅ 正确:使用绝对路径
target_include_directories(mylib PUBLIC ${CMAKE_SOURCE_DIR}/include/)
# 或
target_include_directories(mylib PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/../include/)
问题 2:子目录顺序错误
# ❌ 错误:src 依赖 lib,但 lib 还没定义
add_subdirectory(src) # src 中使用 mylib 目标
add_subdirectory(lib) # mylib 在这里才定义
# ✅ 正确:先定义依赖
add_subdirectory(lib) # 先定义 mylib
add_subdirectory(src) # 再使用 mylib
问题 3:混用旧式和现代方法
# ❌ 不推荐:混用
include_directories(include/) # 旧式
target_include_directories(mylib ...) # 现代
# ✅ 推荐:统一使用现代方法
target_include_directories(mylib PUBLIC include/)
target_include_directories(app PRIVATE src/)
- 1. 优先使用 target_xxx 命令:
target_include_directories而非include_directories,target_compile_options而非add_compile_options - 2. 合理使用作用域:内部实现 → PRIVATE,公共接口 → PUBLIC,仅头文件库 → INTERFACE
- 3. 路径使用变量:
${CMAKE_SOURCE_DIR}、${CMAKE_CURRENT_SOURCE_DIR}、${CMAKE_CURRENT_BINARY_DIR} - 4. 子目录顺序:先定义被依赖的目标,再定义依赖者
- 5. 保持 CMakeLists.txt 简洁:每个子目录管理自己的目标,根目录只负责协调
5. 命令基础
5.1 条件判断
if(WIN32)
message("Windows platform")
elseif(UNIX)
message("Unix-like platform")
else()
message("Other platform")
endif()
# 常用条件
if(DEFINED MY_VAR) # 变量是否定义
if(EXISTS ${FILE_PATH}) # 文件是否存在
if(MY_VAR STREQUAL "value") # 字符串相等
if(MY_VAR MATCHES "regex") # 正则匹配
if(MY_VAR GREATER 10) # 数值比较
5.2 循环
# foreach 循环
set(MY_LIST item1 item2 item3)
foreach(item IN LISTS MY_LIST)
message("Item: ${item}")
endforeach()
# 范围循环
foreach(i RANGE 5)
message("Number: ${i}") # 0, 1, 2, 3, 4, 5
endforeach()
# while 循环
set(i 0)
while(i LESS 5)
message("i = ${i}")
math(EXPR i "${i} + 1")
endwhile()
5.3 函数和宏
函数(有独立作用域)
function(my_function arg1 arg2)
message("Args: ${arg1}, ${arg2}")
set(LOCAL_VAR "local") # 局部变量
endfunction()
my_function("hello" "world")
# LOCAL_VAR 在函数外访问不到
宏(无独立作用域)
macro(my_macro arg)
set(result ${arg}) # 直接修改外部变量
endmacro()
my_macro("value")
message("result = ${result}") # 可以访问
- 函数:有独立作用域,不会污染外部变量(推荐)
- 宏:无作用域,直接文本替换(类似 C 的宏)
5.4 消息输出
message(STATUS "This is a status message")
message(WARNING "This is a warning")
message(FATAL_ERROR "This stops configuration")
message("Simple message")
# 调试输出
message("Variable value: ${MY_VAR}")
6. 总结
1. CMakeLists.txt
- CMake 的配置文件,描述构建规则
- 在配置阶段执行,生成构建系统文件
2. Target(目标)
- CMake 的核心概念,代表构建产物
- 每个 Target 有自己的属性(头文件路径、编译选项、依赖库等)
- Target 之间可以建立依赖关系,自动传递属性
- 作用域(PRIVATE/PUBLIC/INTERFACE):PRIVATE 仅自己用,PUBLIC 自己用依赖者也用,INTERFACE 仅依赖者用
3. 变量系统
- 存储配置信息,提高灵活性
- 分为普通变量、缓存变量、环境变量
- 有作用域概念(全局、函数、目录)
4. 子目录与头文件管理
add_subdirectory:引入子目录的 CMakeLists.txttarget_include_directories:现代头文件管理(推荐)include_directories:旧式全局方法(不推荐)- 子目录中的 Target 对父目录可见
- 变量继承但修改不影响父目录
5. 命令基础
- 条件判断、循环、函数/宏
- 让 CMakeLists.txt 更灵活、更强大
学习路径:
✅ 引入篇:从 GCC 到 Makefile 再到 CMake → 理解为什么需要 CMake
✅ 篇一:快速入门 → 会用 CMake
✅ 篇二:核心概念 → 理解 CMake(当前)
→ 篇三:常用指令与实践 → 精通 CMake
系列文章:
- CMake 学习引入:从 GCC 到 Makefile 再到 CMake
- CMake学习:快速一步一步入门到进阶静态库+动态库(篇一)
- CMake学习篇二:CMake 核心概念(本文)
下一步:
- 学习篇三:掌握常用指令的详细用法
- 实战练习:在真实项目中应用这些概念
最后更新:2026-05-28