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变量系统里最容易让人懵圈的部分。咱们用一张图来理解:
从图上能看出来:
- 根目录变量:默认不传递给子目录。子目录里
${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、循环这些,到时候变量这些知识都会用上。