CMake 系列教程(二):基础命令详解
从单文件到多目标,掌握 CMake 核心命令
一、回顾与准备
上一篇我们完成了一个最简 CMake 项目。现实项目远不止一个 main.cpp------多文件、多目标、头文件搜索路径、链接第三方库......这些都离不开 CMake 的核心命令。
本文所有示例基于 CMake 3.20+,推荐使用现代 CMake 风格。
二、项目定义命令
2.1 cmake_minimum_required
cmake
cmake_minimum_required(VERSION 3.20)
- 必须写在
CMakeLists.txt的最开头 (project()之前) - 设置策略(Policy)行为到对应版本,避免旧版兼容行为
- 可选加
FATAL_ERROR(3.x 中已无实际区别)
2.2 project
cmake
# 完整形式
project(MyApp
VERSION 1.0.0
DESCRIPTION "A demo application"
LANGUAGES CXX C
)
project() 做了很多事:
| 设置的变量 | 值 |
|---|---|
PROJECT_NAME |
MyApp |
PROJECT_VERSION |
1.0.0 |
PROJECT_DESCRIPTION |
"A demo application" |
CMAKE_PROJECT_NAME |
MyApp(顶层项目名) |
CMAKE_CXX_COMPILER |
自动检测的 C++ 编译器 |
⚠️
LANGUAGES不写时默认为C CXX。纯 C 项目记得写LANGUAGES C,可加速配置。
三、目标定义命令
3.1 add_executable
cmake
# 基本用法
add_executable(myapp main.cpp)
# 多个源文件
add_executable(myapp
main.cpp
app.cpp
utils.cpp
)
# 使用 GLOB 自动收集源文件(不推荐,见后文说明)
file(GLOB SOURCES "src/*.cpp")
add_executable(myapp ${SOURCES})
3.2 add_library
cmake
# 静态库(默认)
add_library(math STATIC
math/add.cpp
math/sub.cpp
)
# 动态库(共享库)
add_library(utils SHARED
utils/logger.cpp
utils/config.cpp
)
# 接口库(仅头文件库,无 .cpp 文件)
add_library(fmt INTERFACE)
| 类型 | 关键字 | 产出 | 说明 |
|---|---|---|---|
| 静态库 | STATIC |
.a / .lib |
编译时链接到可执行文件中 |
| 动态库 | SHARED |
.so / .dll |
运行时加载 |
| 接口库 | INTERFACE |
无产物 | 仅传播头文件路径和编译选项 |
3.3 为什么不用 file(GLOB) 收集源文件?
cmake
# ❌ 不推荐
file(GLOB MY_SOURCES "src/*.cpp")
add_executable(myapp ${MY_SOURCES})
原因 :CMake 的配置阶段不会自动重新运行。新增 .cpp 文件后,CMake 不知道需要重新配置,新文件不会被编译 。除非你每次手动 cmake -B build,否则构建会悄悄跳过新文件。
正确做法:显式列出每个源文件。
四、目标属性命令(现代 CMake 核心)
这是现代 CMake 最重要的部分------以目标为中心设置属性,而非全局变量。
4.1 target_include_directories
控制头文件搜索路径:
cmake
add_library(math STATIC math/add.cpp math/sub.cpp)
# PRIVATE:仅 math 目标本身可见
target_include_directories(math PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/math
)
# PUBLIC:math 目标可见,链接 math 的目标也可见
target_include_directories(math PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/math
)
# INTERFACE:math 目标本身不可见,仅传播给链接它的目标
# 适用于纯头文件库
target_include_directories(fmt INTERFACE
${CMAKE_CURRENT_SOURCE_DIR}/include
)
三种可见性:
| 关键字 | 自己 | 链接自己的目标 | 典型场景 |
|---|---|---|---|
PRIVATE |
✅ | ❌ | 内部实现细节 |
PUBLIC |
✅ | ✅ | 对外公开的头文件 |
INTERFACE |
❌ | ✅ | 仅头文件库 |
💡 记忆口诀:PRIVATE 是"我的",PUBLIC 是"大家的",INTERFACE 是"只给别人的"。
4.2 target_link_libraries
链接库,并传播属性:
cmake
add_executable(myapp main.cpp)
# 链接 math 库
target_link_libraries(myapp PRIVATE math)
# 链接系统库(如 pthread)
target_link_libraries(myapp PRIVATE pthread)
# 链接第三方库(通过 find_package 找到的,后文详解)
target_link_libraries(myapp PRIVATE fmt::fmt)
关键理解 :target_link_libraries 不仅链接库文件,还会传播 该库的 PUBLIC/INTERFACE 属性(头文件路径、编译选项等)。
cmake
# 假设 math 声明了 PUBLIC include 路径
target_include_directories(math PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/math)
# myapp 链接 math 后,自动获得 math 的 PUBLIC include 路径
target_link_libraries(myapp PRIVATE math)
# ✅ 无需再手动 target_include_directories
4.3 target_compile_options
设置编译选项:
cmake
# 开启 C++17,警告全部打开
target_compile_options(myapp PRIVATE
-std=c++17
-Wall -Wextra -Wpedantic
)
# 跨平台写法(避免在 MSVC 上传 GCC 选项)
target_compile_options(myapp PRIVATE
$<$<CXX_COMPILER_ID:GNU,Clang>:-Wall -Wextra>
$<$<CXX_COMPILER_ID:MSVC>:/W4>
)
💡
$<$<...>:...>是生成器表达式(Generator Expression),后续文章会详细讲解。
4.4 target_compile_features
更优雅地指定 C++ 标准:
cmake
# 等价于 -std=c++17,但跨平台
target_compile_features(myapp PRIVATE cxx_std_17)
这是设置 C++ 标准的推荐方式 ,比全局设置 CMAKE_CXX_STANDARD 更精确。
4.5 属性传播总结
┌──────────────────────┐
│ math (库目标) │
│ │
│ PUBLIC include: math/ │
│ PUBLIC compile: -O2 │
└──────────┬───────────┘
│
target_link_libraries(myapp PRIVATE math)
│
▼
┌──────────────────────┐
│ myapp (可执行) │
│ │
│ ✅ 获得 math/ 路径 │
│ ✅ 获得 -O2 选项 │
└──────────────────────┘
五、实战:多目标项目
5.1 项目结构
calculator/
├── CMakeLists.txt
├── src/
│ ├── main.cpp
│ ├── core/
│ │ ├── calc.h
│ │ └── calc.cpp
│ └── utils/
│ ├── logger.h
│ └── logger.cpp
└── include/
└── calc/
└── calc.h # 对外公开头文件
5.2 CMakeLists.txt
cmake
cmake_minimum_required(VERSION 3.20)
project(Calculator VERSION 1.0.0 LANGUAGES CXX)
# --- 库:core ---
add_library(calc STATIC
src/core/calc.cpp
)
target_include_directories(calc PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/include # 对外公开
)
target_include_directories(calc PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src/core # 内部使用
)
target_compile_features(calc PUBLIC cxx_std_17)
# --- 库:utils ---
add_library(utils STATIC
src/utils/logger.cpp
)
target_include_directories(utils PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/src/utils
)
target_compile_features(utils PRIVATE cxx_std_17)
# --- 可执行文件 ---
add_executable(calculator src/main.cpp)
target_link_libraries(calculator PRIVATE calc utils)
# calculator 自动获得 calc 和 utils 的 PUBLIC 属性
5.3 关键要点
- 库目标自己声明自己需要的头文件路径,而非让消费者手动添加
PUBLIC和PRIVATE精确控制传播范围target_compile_features(calc PUBLIC cxx_std_17)--- 库要求 C++17,链接它的目标自动继承此要求- 消费者(
calculator)只需target_link_libraries,无需关心依赖细节
六、构建类型
6.1 四种标准构建类型
| 类型 | 优化 | 调试信息 | 用途 |
|---|---|---|---|
Debug |
❌ | ✅ | 开发调试 |
Release |
✅ | ❌ | 正式发布 |
RelWithDebInfo |
✅ | ✅ | 发布但保留调试信息 |
MinSizeRel |
✅(体积优先) | ❌ | 嵌入式/体积敏感场景 |
6.2 设置构建类型
bash
# 配置时指定
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build
cmake
# CMakeLists.txt 中设置默认值
if(NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Debug CACHE STRING "Build type" FORCE)
endif()
⚠️ 单配置生成器 (Makefile、Ninja)通过
CMAKE_BUILD_TYPE切换,需要重新配置。多配置生成器(Visual Studio、Xcode)在构建时指定:
bashcmake -B build -G "Visual Studio 17 2022" cmake --build build --config Release
七、子目录与多 CMakeLists.txt
7.1 add_subdirectory
当项目变大,可以拆分为多个子目录,每个子目录有自己的 CMakeLists.txt:
project/
├── CMakeLists.txt # 顶层
├── src/
│ ├── CMakeLists.txt # 定义可执行目标
│ └── main.cpp
└── libs/
├── math/
│ ├── CMakeLists.txt # 定义 math 库目标
│ └── math.cpp
└── utils/
├── CMakeLists.txt # 定义 utils 库目标
└── logger.cpp
顶层 CMakeLists.txt:
cmake
cmake_minimum_required(VERSION 3.20)
project(MyProject LANGUAGES CXX)
add_subdirectory(libs/math)
add_subdirectory(libs/utils)
add_subdirectory(src)
libs/math/CMakeLists.txt:
cmake
add_library(math STATIC math.cpp)
target_include_directories(math PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_compile_features(math PUBLIC cxx_std_17)
src/CMakeLists.txt:
cmake
add_executable(myapp main.cpp)
target_link_libraries(myapp PRIVATE math utils)
7.2 重要的目录变量
| 变量 | 含义 |
|---|---|
CMAKE_CURRENT_SOURCE_DIR |
当前 CMakeLists.txt 所在目录(源码侧) |
CMAKE_CURRENT_BINARY_DIR |
当前 CMakeLists.txt 对应的构建目录 |
CMAKE_SOURCE_DIR |
最顶层 CMakeLists.txt 所在目录 |
CMAKE_BINARY_DIR |
最顶层的构建目录 |
PROJECT_SOURCE_DIR |
最近一次 project() 调用所在目录 |
💡 最佳实践 :优先使用
CMAKE_CURRENT_SOURCE_DIR和PROJECT_SOURCE_DIR,避免使用CMAKE_SOURCE_DIR(后者在项目被其他项目通过add_subdirectory引入时会指错位置)。
八、常用辅助命令速查
| 命令 | 用途 | 示例 |
|---|---|---|
message |
输出信息 | message(STATUS "Configuring...") |
set |
设置变量 | set(SOURCES a.cpp b.cpp) |
list |
列表操作 | list(APPEND SOURCES c.cpp) |
option |
定义开关 | option(ENABLE_TESTS "Build tests" ON) |
configure_file |
生成配置头文件 | configure_file(config.h.in config.h) |
add_custom_command |
自定义构建步骤 | 代码生成、预处理等 |
install |
安装规则 | 安装到系统路径 |
小结
| 核心命令 | 作用 | 关键点 |
|---|---|---|
add_executable |
定义可执行目标 | 显式列出源文件 |
add_library |
定义库目标 | STATIC / SHARED / INTERFACE |
target_include_directories |
头文件搜索路径 | PRIVATE / PUBLIC / INTERFACE |
target_link_libraries |
链接库 + 传播属性 | 优先用 PRIVATE |
target_compile_features |
设置 C++ 标准 | 比全局变量更精确 |
add_subdirectory |
添加子目录 | 模块化管理 |
现代 CMake 的核心思想 :所有属性挂载在目标上,通过 target_* 命令设置,通过链接关系自动传播。减少全局变量,让依赖关系显式、可追溯。
📖 下一期预告:《CMake 系列教程(三):变量、条件与控制流》------ 讲解 CMake 的变量机制、条件判断、循环、函数与宏,让你的构建脚本具备复杂逻辑处理能力。