4、变量与缓存:定义变量、缓存变量、环境变量、变量作用域

变量这东西,说白了就是CMake里的「临时便签」。你写个值上去,后面随时能取出来用。但CMake的变量系统比你想的要复杂一点——它不光有普通变量,还有缓存变量、环境变量,而且每个变量都有自己的「活动范围」。嗯,咱们今天就把这些掰扯清楚。

4.1 普通变量:最常用的便签

普通变量用 set() 命令定义,用 ${} 取出来。我刚开始用CMake时,觉得这跟C语言的宏定义差不多,后来才发现完全不是一回事。

set(MY_VAR "hello")
message("${MY_VAR}")  # 输出 hello

这里有个坑:变量名是大小写敏感的。MY_VAR 和 my_var 是两个不同的变量。我曾经在项目里因为大小写不一致,排查了半天才发现是变量名写错了。

普通变量的作用域是「当前CMakeLists.txt文件及它调用的子目录」。什么意思呢?你在根目录定义了一个变量,子目录默认是看不到的。除非你用 PARENT_SCOPE 往上传递,或者用 add_subdirectory() 时注意作用域规则。

小技巧: 如果你想让变量在多个子目录间共享,可以考虑用 set() 加 CACHE 参数,或者用 target_compile_definitions() 传给编译器。

4.2 缓存变量:持久化的秘密

缓存变量跟普通变量最大的区别是:它会被写入 CMakeCache.txt 文件。你下次再运行cmake时,这个值还在。普通变量?跑完就没了。

set(MY_CACHE_VAR "default_value" CACHE STRING "这是一个缓存变量")

缓存变量通常用来让用户配置项目选项。比如你写个库,想让用户选择是否启用某个功能:

option(ENABLE_FEATURE_X "启用X功能" ON)
# 这其实等价于:
# set(ENABLE_FEATURE_X ON CACHE BOOL "启用X功能")

我个人习惯把缓存变量放在项目根目录的CMakeLists.txt里,用 option() 或 set(... CACHE ...) 定义。这样用户用 cmake -D 就能覆盖默认值。

注意: 缓存变量一旦设置,就不会被后续的 set() 覆盖。除非你加 FORCE 参数。我曾经因为这个特性,在重构项目时发现某个缓存变量死活改不过来,最后只能手动删掉CMakeCache.txt。

4.3 环境变量:跟操作系统打交道

环境变量用 $ENV{} 读取,用 set(ENV{...}) 设置。它跟CMake的普通变量、缓存变量是两套系统。

message("PATH: $ENV{PATH}")
set(ENV{MY_ENV_VAR} "hello from cmake")
message("MY_ENV_VAR: $ENV{MY_ENV_VAR}")

环境变量有个特点:你在CMake里设置的环境变量,只对当前cmake进程和它启动的子进程有效。不会影响你终端的环境变量。所以别指望用CMake改环境变量来影响后续的构建。

我在项目中遇到过一个问题:某个第三方库需要 LD_LIBRARY_PATH 环境变量才能找到动态库。我一开始在CMake里用 set(ENV{LD_LIBRARY_PATH} ...),结果发现根本没用。后来才明白,环境变量要在调用 add_custom_command() 或 add_custom_target() 时通过 ENVIRONMENT 参数传递才行。

4.4 变量作用域:你的变量到底在哪生效?

作用域是CMake变量系统里最容易让人懵圈的部分。咱们用一张图来理解:

根目录 CMakeLists.txt set(ROOT_VAR "根变量") add_subdirectory(sub) 子目录 sub/CMakeLists.txt set(SUB_VAR "子变量") # 这里看不到 ROOT_VAR! 函数内部 function(my_func) ... endfunction()

从图上能看出来:

  • 根目录变量:默认不传递给子目录。子目录里 ${ROOT_VAR} 是空的。
  • 子目录变量:只在子目录及其更深层的子目录里有效。不会影响父目录。
  • 函数内变量:只在函数内部有效。函数返回后变量就没了。

那怎么让变量跨作用域传递呢?有三种方式:

方式 命令 说明
向上传递 set(VAR "val" PARENT_SCOPE) 在子目录或函数里,把变量传给父作用域
缓存变量 set(VAR "val" CACHE ...) 全局可见,所有CMakeLists.txt都能访问
全局属性 set_property(GLOBAL ...) 更高级的全局共享方式,适合复杂场景
核心原则: 变量默认向下传递(父→子),不向上传递(子→父)。想往上走?用 PARENT_SCOPE 或者缓存变量。

4.5 实战:一个完整的变量使用示例

咱们写个实际点的例子。假设你有个项目,需要让用户选择是否启用测试,并且测试依赖某个外部库:

# 根目录 CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(VariableDemo)

# 缓存变量:用户可配置
option(ENABLE_TESTS "启用单元测试" ON)
set(TEST_LIB_PATH "" CACHE PATH "测试库的路径")

# 普通变量:只在根目录有效
set(PROJECT_VERSION_MAJOR 1)
set(PROJECT_VERSION_MINOR 0)

# 根据用户选择设置
if(ENABLE_TESTS)
  message(STATUS "测试已启用")
  add_subdirectory(tests)
else()
  message(STATUS "测试已禁用")
endif()

# 子目录
add_subdirectory(src)
# src/CMakeLists.txt
# 这里能看到 ENABLE_TESTS 吗?能!因为它是缓存变量
# 但看不到 PROJECT_VERSION_MAJOR,因为它是普通变量且没传下来

# 如果想用根目录的版本号,可以这样:
set(PROJECT_VERSION_MAJOR 1 PARENT_SCOPE)  # 或者用缓存变量

# 或者更推荐的做法:用 target_compile_definitions 传给源码
add_executable(my_app main.cpp)
target_compile_definitions(my_app PRIVATE
  VERSION_MAJOR=${PROJECT_VERSION_MAJOR}
  VERSION_MINOR=${PROJECT_VERSION_MINOR}
)

这个例子里,ENABLE_TESTS 是缓存变量,所以子目录也能读到。而 PROJECT_VERSION_MAJOR 是普通变量,子目录默认看不到。我建议把需要跨目录共享的变量都做成缓存变量,或者用 target_* 命令直接传给目标。

4.6 避坑指南

最后分享几个我踩过的坑:

  • 变量展开顺序:${VAR} 是在解析时展开的,不是运行时。所以 set(VAR "${OTHER_VAR}") 时,OTHER_VAR 必须已经定义好了。
  • 缓存变量覆盖:如果你用 cmake -D 设置了缓存变量,后续的 set(... CACHE ...) 不会覆盖它。想强制覆盖?加 FORCE。
  • 环境变量陷阱:CMake里 set(ENV{PATH} ...) 只影响当前进程。想影响构建命令?用 ENVIRONMENT 参数。
  • 作用域误解:很多人以为 add_subdirectory() 会把父目录的所有变量传下去。其实不会。只有缓存变量和通过 PARENT_SCOPE 传递的变量才能跨目录。

嗯,变量这部分内容就这些。说白了就是记住三句话:普通变量用完就丢,缓存变量持久保存,环境变量跟系统打交道。作用域嘛,记住「向下传递,不向上」就行了。下一章咱们聊聊控制流——if、循环这些,到时候变量这些知识都会用上。