现在位置: 首页 > CMake 教程 > 正文

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 最低版本:

实例

# 语法:cmake_minimum_required(VERSION <version>)
# 该指令必须放在 CMakeLists.txt 的最顶部
# 示例:要求 CMake 版本不低于 3.10
cmake_minimum_required(VERSION 3.10)

定义项目名称和语言:

实例

# 语法:project(<项目名> [<语言>...])
# 语言参数可选,常用值:CXX(C++)、C(C 语言)
# 调用 project() 后会自动设置 PROJECT_NAME 等变量
project(MyProject CXX)

添加可执行文件:

实例

# 语法:add_executable(<目标名> <源文件>...)
# 将指定的源文件编译生成一个可执行文件
add_executable(MyApp main.cpp utils.cpp)

添加库文件:

实例

# 语法:add_library(<目标名> [STATIC | SHARED | MODULE] <源文件>...)
# STATIC:静态库(.a / .lib),编译时直接嵌入可执行文件
# SHARED:动态库(.so / .dll),运行时加载
# 不指定类型时,由 BUILD_SHARED_LIBS 变量决定
add_library(MyLib STATIC library.cpp)

链接库到目标:

实例

# 语法:target_link_libraries(<目标> <库>...)
# 将指定的库链接到目标(可执行文件或其他库)
# 可以链接自己项目中的库目标,也可以链接外部库
target_link_libraries(MyApp PRIVATE MyLib)

设置变量:

实例

# 语法:set(<变量名> <值>...)
# 定义一个普通变量,后续通过 ${变量名} 引用
set(CMAKE_CXX_STANDARD 17)

# 同时设置多个值(列表)
set(SOURCES main.cpp utils.cpp helper.cpp)
add_executable(MyApp ${SOURCES})

为目标指定头文件路径:

实例

# 语法:target_include_directories(<目标>
#           [BEFORE | AFTER]
#           [SYSTEM]
#           [PUBLIC | PRIVATE | INTERFACE]
#           <路径>...)
# PUBLIC:当前目标和依赖它的目标都能使用该路径
# PRIVATE:只有当前目标能使用
# INTERFACE:只有依赖当前目标的其他目标能使用
target_include_directories(MyApp PRIVATE ${PROJECT_SOURCE_DIR}/include)

设置安装规则:

实例

# 语法:install(TARGETS <目标>...
#         [RUNTIME DESTINATION <可执行文件安装路径>]
#         [LIBRARY DESTINATION <动态库安装路径>]
#         [ARCHIVE DESTINATION <静态库安装路径>]
#         [INCLUDES DESTINATION <头文件安装路径>])
# 定义执行 make install 时各类型文件的安装位置
install(TARGETS MyApp RUNTIME DESTINATION bin)

条件语句:

实例

# 语法:if(<条件>) ... elseif(<条件>) ... else() ... endif()
# 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()

自定义命令:

实例

# 语法:add_custom_command(
#          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 示例,综合使用了上述多个指令。

实例

# 文件路径:项目根目录/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 才能让新默认值生效。

实例

# 语法:set(<变量名> <默认值> CACHE <类型> <描述>)
# 类型可选: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 自身需要 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() 会搜索系统中已安装的库,并设置对应的变量(如头文件路径、库文件路径、库目标名称)。

实例

# 基本用法:查找 Boost 库(不限定版本)
# 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 库。

实例

# 文件路径:项目根目录/CMakeLists.txt
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。