CMake 基础
CMake 是一个跨平台的构建系统生成工具,用于管理 C/C++ 项目的编译过程。
通过编写 CMakeLists.txt 配置文件,你可以定义项目的构建规则、依赖关系和编译选项,CMake 会自动生成对应平台的构建文件(如 Unix 下的 Makefile 或 Windows 下的 Visual Studio 解决方案)。
CMakeLists.txt 文件
CMakeLists.txt 是 CMake 的核心配置文件,每个 CMake 项目至少需要一个 CMakeLists.txt 文件。
CMake 通过读取文件中的指令来了解项目的结构、源文件列表和编译要求。
基本语法
CMakeLists.txt 由一系列 CMake 指令组成,每个指令的格式为:命令名(参数列表)。
下面是构建 CMake 项目时最常用的指令及其实例。
指定 CMake 最低版本:
实例
# 该指令必须放在 CMakeLists.txt 的最顶部
# 示例:要求 CMake 版本不低于 3.10
cmake_minimum_required(VERSION 3.10)
定义项目名称和语言:
实例
# 语言参数可选,常用值:CXX(C++)、C(C 语言)
# 调用 project() 后会自动设置 PROJECT_NAME 等变量
project(MyProject CXX)
添加可执行文件:
实例
# 将指定的源文件编译生成一个可执行文件
add_executable(MyApp main.cpp utils.cpp)
添加库文件:
实例
# STATIC:静态库(.a / .lib),编译时直接嵌入可执行文件
# SHARED:动态库(.so / .dll),运行时加载
# 不指定类型时,由 BUILD_SHARED_LIBS 变量决定
add_library(MyLib STATIC library.cpp)
链接库到目标:
实例
# 将指定的库链接到目标(可执行文件或其他库)
# 可以链接自己项目中的库目标,也可以链接外部库
target_link_libraries(MyApp PRIVATE MyLib)
设置变量:
实例
# 定义一个普通变量,后续通过 ${变量名} 引用
set(CMAKE_CXX_STANDARD 17)
# 同时设置多个值(列表)
set(SOURCES main.cpp utils.cpp helper.cpp)
add_executable(MyApp ${SOURCES})
为目标指定头文件路径:
实例
# [BEFORE | AFTER]
# [SYSTEM]
# [PUBLIC | PRIVATE | INTERFACE]
# <路径>...)
# PUBLIC:当前目标和依赖它的目标都能使用该路径
# PRIVATE:只有当前目标能使用
# INTERFACE:只有依赖当前目标的其他目标能使用
target_include_directories(MyApp PRIVATE ${PROJECT_SOURCE_DIR}/include)
设置安装规则:
实例
# [RUNTIME DESTINATION <可执行文件安装路径>]
# [LIBRARY DESTINATION <动态库安装路径>]
# [ARCHIVE DESTINATION <静态库安装路径>]
# [INCLUDES DESTINATION <头文件安装路径>])
# 定义执行 make install 时各类型文件的安装位置
install(TARGETS MyApp RUNTIME DESTINATION bin)
条件语句:
实例
# CMake 支持的条件表达式包括:比较、逻辑运算、变量判断等
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
# Debug 模式下启用调试信息
message(STATUS "当前为 Debug 构建模式")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g")
else()
# Release 模式下启用优化
message(STATUS "当前为 Release 构建模式")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O2")
endif()
自定义命令:
实例
# TARGET <目标>
# PRE_BUILD | PRE_LINK | POST_BUILD
# COMMAND <命令> [参数...]
# [COMMENT <说明>]
# [VERBATIM])
# PRE_BUILD:在编译前执行
# PRE_LINK:在链接前执行
# POST_BUILD:在构建完成后执行
add_custom_command(
TARGET MyApp POST_BUILD
COMMAND ${CMAKE_COMMAND} -E echo "构建完成!"
COMMENT "打印构建完成信息"
)
完整实例
下面是一个完整的 CMakeLists.txt 示例,综合使用了上述多个指令。
实例
# 1. 指定 CMake 最低版本(必须放在最前面)
cmake_minimum_required(VERSION 3.10)
# 2. 定义项目名称、版本和语言
project(MyProject VERSION 1.0.0 LANGUAGES CXX)
# 3. 设置 C++ 标准为 C++17,并强制要求使用该标准
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# 4. 添加静态库目标
add_library(MyLib STATIC
src/lib/core.cpp # 核心功能实现
src/lib/helper.cpp # 辅助函数实现
)
# 5. 添加可执行文件目标
add_executable(MyApp
src/main.cpp # 程序入口
src/utils.cpp # 工具函数
)
# 6. 为库目标指定公开头文件路径
# PUBLIC 表示使用 MyLib 的目标也能访问该路径
target_include_directories(MyLib PUBLIC ${PROJECT_SOURCE_DIR}/include)
# 7. 为可执行文件指定私有头文件路径
target_include_directories(MyApp PRIVATE ${PROJECT_SOURCE_DIR}/src)
# 8. 将 MyLib 链接到 MyApp
target_link_libraries(MyApp PRIVATE MyLib)
# 9. 设置安装规则
install(TARGETS MyApp RUNTIME DESTINATION bin)
install(TARGETS MyLib ARCHIVE DESTINATION lib)
构建这个项目的典型命令如下:
# 在项目根目录下创建 build 目录(推荐外部构建) $ mkdir build && cd build # 生成构建文件 $ cmake .. # 编译项目 $ cmake --build . # 安装到系统目录(可选) $ cmake --install .
变量和缓存
CMake 使用变量来存储和传递配置信息,变量可以在 CMakeLists.txt 中定义和使用。
变量分为两类:普通变量和缓存变量,它们的作用域和生命周期不同。
普通变量
普通变量在 CMakeLists.txt 中定义,作用域为当前目录及其子目录。
普通变量在每次 CMake 运行时重新计算,不会持久化。
实例
set(MY_APP_NAME "runoob-app")
# 定义一个列表变量(用分号或空格分隔)
set(SOURCE_FILES main.cpp utils.cpp helper.cpp)
# 使用变量:通过 ${变量名} 引用
message(STATUS "项目名称:${MY_APP_NAME}")
# 使用列表变量:循环遍历每个源文件
foreach(FILE ${SOURCE_FILES})
message(STATUS "源文件:${FILE}")
endforeach()
缓存变量
缓存变量存储在 CMakeCache.txt 文件中,在多次 CMake 运行之间保持值不变。
缓存变量通常用于让用户在 CMake 配置阶段自定义构建设置,例如开关功能或指定安装路径。
缓存变量的值会持久化。如果你修改了 CMakeLists.txt 中缓存变量的默认值,需要删除 CMakeCache.txt 或重新运行 cmake 才能让新默认值生效。
实例
# 类型可选:STRING、BOOL、PATH、FILEPATH
# 如果 CMakeCache.txt 中已有该变量,set 不会覆盖已有值
# 定义一个字符串类型的缓存变量(用户可修改的安装路径)
set(MY_INSTALL_PATH "/usr/local" CACHE PATH "程序的安装路径")
# 定义一个布尔类型的缓存变量(功能开关)
set(ENABLE_LOGGING ON CACHE BOOL "是否启用日志功能")
# 使用缓存变量和其他变量一样
if(ENABLE_LOGGING)
message(STATUS "日志功能已启用")
add_definitions(-DENABLE_LOGGING)
endif()
用户可以通过命令行 -D 参数修改缓存变量:
# 在 cmake 命令中通过 -D 修改缓存变量 $ cmake .. -DMY_INSTALL_PATH=/opt/runoob -DENABLE_LOGGING=OFF
头文件搜索路径
在 CMake 中,有两种方式可以指定编译器查找头文件的路径:include_directories() 和 target_include_directories()。
了解两者的区别对于编写清晰、可维护的 CMake 项目非常重要。
include_directories 与 target_include_directories 对比
两者都可以为编译器添加头文件搜索路径,但作用范围和控制粒度不同。
| 特性 | include_directories() |
target_include_directories() |
|---|---|---|
| 作用范围 | 全局作用域,影响当前目录及子目录中的所有目标 | 仅作用于指定的目标,不污染其他目标 |
| 推荐程度 | 不推荐,除非维护旧版 CMake 项目 | 推荐优先使用,符合现代 CMake 最佳实践 |
| 目标关联性 | 不直接关联到特定目标,可能意外影响其他目标 | 显式绑定到指定目标,依赖关系一目了然 |
| 可维护性 | 较差,容易导致全局路径污染,问题难以排查 | 较好,路径与目标绑定,逻辑清晰 |
| 可见性控制 | 无法精确控制传播范围 | 通过 PUBLIC、PRIVATE、INTERFACE 精确控制 |
PUBLIC / PRIVATE / INTERFACE 的含义
target_include_directories() 的关键优势在于可以通过可见性关键字精确控制头文件路径的传播。
| 关键字 | 当前目标可用 | 依赖此目标的其他目标可用 | 典型场景 |
|---|---|---|---|
PRIVATE |
是 | 否 | 仅自身实现需要的内部头文件 |
PUBLIC |
是 | 是 | 库的公开 API 头文件,自身和消费者都需要 |
INTERFACE |
否 | 是 | 仅头文件的库(Header-only library) |
实例
# MyLib 自身需要 include/,使用 MyLib 的 MyApp 也需要 include/
# PUBLIC:自身和消费者都能访问该路径
target_include_directories(MyLib PUBLIC ${PROJECT_SOURCE_DIR}/include)
# PRIVATE:只有 MyApp 自己能访问(内部工具函数头文件)
target_include_directories(MyApp PRIVATE ${PROJECT_SOURCE_DIR}/src/internal)
# INTERFACE:MyLib 是纯头文件库,自身无需编译,只传递路径给消费者
add_library(HeaderOnlyLib INTERFACE)
target_include_directories(HeaderOnlyLib INTERFACE ${PROJECT_SOURCE_DIR}/header_only)
查找库和包
很多项目依赖外部库(如 Boost、OpenCV),CMake 提供了 find_package() 指令来自动检测和配置这些外部依赖。
find_package 指令
find_package() 会搜索系统中已安装的库,并设置对应的变量(如头文件路径、库文件路径、库目标名称)。
实例
# REQUIRED 表示如果找不到则中止 CMake 配置
find_package(Boost REQUIRED)
# 指定最低版本:要求 Boost 版本不低于 1.70
find_package(Boost 1.70 REQUIRED)
# 指定搜索路径:在自定义路径下查找 OpenCV
find_package(OpenCV REQUIRED PATHS /path/to/opencv)
使用查找到的库
find_package() 成功后,通常会提供以下内容供使用:
| 提供的内容 | 示例 | 说明 |
|---|---|---|
| 导入目标 | Boost::Boost |
现代 CMake 推荐方式,直接通过 target_link_libraries 使用 |
| 头文件路径变量 | ${Boost_INCLUDE_DIRS} |
旧式用法,配合 include_directories 使用 |
| 库文件路径变量 | ${Boost_LIBRARY_DIRS} |
旧式用法,配合 link_directories 使用 |
强烈推荐使用导入目标方式(如 Boost::Boost),而不是手动拼接头文件和库路径。导入目标会自动处理所有编译和链接选项,减少配置错误。
使用第三方库示例
下面是一个完整的示例,展示如何在 CMake 项目中引入并使用 Boost 库。
实例
cmake_minimum_required(VERSION 3.10)
project(MyBoostProject CXX)
# 设置 C++ 标准
set(CMAKE_CXX_STANDARD 17)
# 查找 Boost 库,如果找不到则报错退出
find_package(Boost REQUIRED)
# 添加可执行文件
add_executable(MyApp main.cpp)
# 使用导入目标链接 Boost
# Boost::Boost 会自动设置头文件路径和库文件路径
target_link_libraries(MyApp PRIVATE Boost::Boost)
注意事项
优先使用 target_xxx 系列指令。include_directories() 和 link_directories() 是全局指令,容易导致路径污染。在大型项目中应始终使用 target_include_directories() 和 target_link_libraries(),它们让依赖关系更清晰、更易维护。
始终指定 C++ 标准。如果不设置 CMAKE_CXX_STANDARD,编译器会使用默认标准(通常是 C++98 或 C++14,取决于编译器版本)。建议同时设置 CMAKE_CXX_STANDARD_REQUIRED 为 ON,确保编译器不支持指定标准时直接报错。
理解 PUBLIC / PRIVATE / INTERFACE 的区别。错误使用这些关键字是 CMake 初学者最常见的坑。简单记忆:自身需要且消费者需要 → PUBLIC,只有自身需要 → PRIVATE,只有消费者需要(如纯头文件库)→ INTERFACE。
缓存变量的默认值不会自动更新。修改 CMakeLists.txt 中 set(... CACHE ...) 的默认值后,已有的 CMakeCache.txt 不会被覆盖。需要删除 build 目录中的 CMakeCache.txt 或整个 build 目录后重新运行 cmake。
