3、CMake变量与缓存:set()命令、变量作用域、缓存变量、CMake预设变量

好,咱们今天来聊聊CMake里最基础也最容易踩坑的东西——变量。

说实话,我刚开始用CMake那会儿,觉得变量嘛,不就是存个值吗?跟Shell脚本差不多。结果一上手就懵了:为什么我在子目录设了个变量,主CMakeLists.txt里读不到?为什么我改了变量值,重新cmake一下又变回去了?

嗯,这些坑我都踩过。今天咱们就把变量这摊事儿彻底捋清楚。

3.1 set()命令:变量的基本操作

CMake里定义变量,最常用的就是set()命令。语法很简单:

set(<variable> <value>... [PARENT_SCOPE])

举个例子:

set(MY_NAME "CMake实战")
set(MY_SOURCES main.cpp utils.cpp helper.cpp)
set(MY_FLAG ON)

这里要注意一点:set()可以一次给变量赋多个值,这些值会以分号分隔的列表形式存储。说白了,MY_SOURCES实际上存的是 "main.cpp;utils.cpp;helper.cpp"

我在项目中经常看到有人这么写:

set(MY_SOURCES main.cpp)
set(MY_SOURCES ${MY_SOURCES} utils.cpp)
set(MY_SOURCES ${MY_SOURCES} helper.cpp)

这样写没问题,但啰嗦。我个人习惯直接一行搞定:

set(MY_SOURCES main.cpp utils.cpp helper.cpp)
小技巧: 如果你想把多个变量拼接成一个路径,可以用 set() 配合 ${} 展开。比如:
set(BASE_DIR "/home/user/project")
set(SRC_DIR "${BASE_DIR}/src")
这样 SRC_DIR 的值就是 /home/user/project/src

3.2 变量作用域:为什么子目录的变量父目录看不到?

这是新手最容易困惑的地方。CMake的变量作用域,说白了就是:每个目录有自己的变量空间

你想想看,你在 src/CMakeLists.txt 里设了一个变量:

# src/CMakeLists.txt
set(SUBDIR_VAR "我在子目录")

然后在顶层的 CMakeLists.txt 里想用这个变量:

# CMakeLists.txt
add_subdirectory(src)
message("SUBDIR_VAR = ${SUBDIR_VAR}")  # 输出为空!

为什么会这样?因为 add_subdirectory() 会创建一个新的作用域。子目录里定义的变量,默认不会传递到父目录。

那怎么把子目录的变量传出来呢?有两种方式:

  1. 使用 PARENT_SCOPE:在子目录里这样写:
# src/CMakeLists.txt
set(SUBDIR_VAR "我在子目录" PARENT_SCOPE)

这样变量就会传递到父作用域。注意,PARENT_SCOPE 只向上传递一层,不会跨多级。

  1. 使用缓存变量:这个我们马上讲。
我曾经踩过的坑: 有一次我在子目录里用 set() 修改了一个父目录定义的变量,以为父目录也会跟着变。结果调试了半天才发现,子目录的修改只影响子目录自己的副本。从那以后,我养成了一个习惯:凡是需要在多个目录间共享的变量,要么用 PARENT_SCOPE,要么用缓存变量。

3.3 缓存变量:让变量值持久化

普通变量只在当前CMake运行期间有效。你重新运行 cmake 命令,变量值就丢了。但缓存变量不同,它会写入 CMakeCache.txt 文件,下次运行 cmake 时还能读到。

定义缓存变量的语法:

set(<variable> <value> CACHE <type> <docstring> [FORCE])

其中 <type> 可以是 BOOLSTRINGFILEPATHPATH 等。

举个例子:

set(MY_OPTION ON CACHE BOOL "是否启用某个功能")
set(MY_PATH "/usr/local" CACHE PATH "安装路径")

这里有个关键点:如果缓存变量已经存在,set() 不会覆盖它。也就是说,第一次运行 cmake 时变量被设为 ON,第二次运行时你想改成 OFF,但 set() 不会生效——除非你加上 FORCE

set(MY_OPTION OFF CACHE BOOL "是否启用某个功能" FORCE)

加了 FORCE 之后,每次运行 cmake 都会强制覆盖缓存中的值。我个人建议:除非你明确知道自己在做什么,否则别轻易用 FORCE。因为这会破坏用户通过 cmake -D 设置的变量。

核心区别总结:
特性 普通变量 缓存变量
存储位置 内存 CMakeCache.txt
跨运行持久化
作用域 当前目录 全局
用户可通过 -D 修改
FORCE 关键字 强制覆盖

3.4 CMake预设变量:那些CMake帮你定义好的变量

CMake内置了很多预设变量,你直接拿来用就行。我挑几个最常用的说说:

3.4.1 编译器相关

  • CMAKE_CXX_STANDARD:指定C++标准,比如 172023
  • CMAKE_C_STANDARD:指定C标准。
  • CMAKE_CXX_COMPILER:C++编译器路径。
  • CMAKE_C_COMPILER:C编译器路径。

用法示例:

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)  # 如果编译器不支持C++17,直接报错
set(CMAKE_CXX_EXTENSIONS OFF)        # 禁用编译器扩展

我个人习惯在项目一开始就设好这些。你想想看,如果团队成员用的编译器版本不一样,不统一标准的话,编译结果可能千奇百怪。

3.4.2 构建类型相关

  • CMAKE_BUILD_TYPE:构建类型,常见值有 DebugReleaseRelWithDebInfoMinSizeRel

用法:

# 在命令行中设置
# cmake -DCMAKE_BUILD_TYPE=Debug ..

# 或者在CMakeLists.txt中设置默认值
if(NOT CMAKE_BUILD_TYPE)
  set(CMAKE_BUILD_TYPE "Release" CACHE STRING "构建类型" FORCE)
endif()

这里有个小细节:CMAKE_BUILD_TYPE 只在单配置生成器(如Makefile、Ninja)中有效。对于Visual Studio这种多配置生成器,它会被忽略。

3.4.3 安装路径相关

  • CMAKE_INSTALL_PREFIX:安装根目录,默认是 /usr/local(Linux)或 C:/Program Files(Windows)。
  • CMAKE_INSTALL_BINDIR:可执行文件安装目录。
  • CMAKE_INSTALL_LIBDIR:库文件安装目录。

用法:

# 用户可以在命令行指定
# cmake -DCMAKE_INSTALL_PREFIX=/opt/myapp ..

# 项目中也可以设置默认值
set(CMAKE_INSTALL_PREFIX "/usr/local" CACHE PATH "安装路径")

3.4.4 其他常用预设变量

变量名 说明
CMAKE_SOURCE_DIR 顶层CMakeLists.txt所在目录
CMAKE_BINARY_DIR 构建目录(运行cmake的目录)
CMAKE_CURRENT_SOURCE_DIR 当前处理的CMakeLists.txt所在目录
CMAKE_CURRENT_BINARY_DIR 当前处理目录对应的构建目录
PROJECT_NAME 当前project()命令指定的项目名
PROJECT_SOURCE_DIR 当前project()命令所在目录
PROJECT_BINARY_DIR 当前project()命令对应的构建目录
我的经验: 在写CMakeLists.txt时,尽量用 CMAKE_CURRENT_SOURCE_DIRCMAKE_CURRENT_BINARY_DIR,而不是 CMAKE_SOURCE_DIRCMAKE_BINARY_DIR。为什么?因为如果你的项目被其他项目通过 add_subdirectory() 引用,CMAKE_SOURCE_DIR 指向的是别人的顶层目录,而不是你的。用 CURRENT 版本更安全。

3.5 知识体系总览

下面这张图把本章的核心逻辑串起来了:

CMake变量体系 普通变量 缓存变量 预设变量 • set() 定义 • 作用域:当前目录 • PARENT_SCOPE 传递 • 不持久化 • set(... CACHE ...) 定义 • 写入 CMakeCache.txt • 跨运行持久化 • FORCE 强制覆盖 • CMAKE_CXX_STANDARD • CMAKE_BUILD_TYPE • CMAKE_INSTALL_PREFIX • CMAKE_CURRENT_*_DIR 核心原则:普通变量管局部,缓存变量管全局,预设变量直接用 变量作用域:子目录 → 父目录(PARENT_SCOPE) 缓存变量:用户可通过 cmake -D 在命令行修改

3.6 避坑指南

最后,我把自己这些年踩过的坑总结一下:

  • 别在子目录里随意修改父目录的变量——除非你用 PARENT_SCOPE,否则改了也白改。
  • 缓存变量不是用来存临时数据的——它会被写入文件,如果你频繁修改,会导致 CMakeCache.txt 混乱。
  • 预设变量不要随意覆盖——比如 CMAKE_SOURCE_DIR 是只读的,你改了也没用。
  • 注意变量展开的顺序——set(A ${B}) 时,B 必须在之前定义过,否则 A 会是空字符串。

嗯,关于变量这块,今天就聊到这儿。记住一句话:普通变量管局部,缓存变量管全局,预设变量直接用。搞清楚了这些,CMake的变量体系你就拿下了。


公众号:蓝海资料掘金营,微信deep3321