3、CMake变量基础:变量的定义与引用、set()命令、unset()命令、变量作用域(函数/目录/全局)、变量引用机制(${})
好,咱们今天来聊聊CMake里最基础、也最核心的东西——变量。
说实话,我见过不少新手,一上来就写set(SOURCES main.cpp utils.cpp),然后到处用${SOURCES},觉得挺简单。但真到了项目复杂起来,变量作用域搞混、缓存变量覆盖、引用嵌套出问题……这些坑我当年都踩过。
所以这一章,咱们把变量这块地基打牢。你想想看,后面30个章节,哪个能离开变量?
3.1 变量的定义与引用:最朴素的用法
CMake里定义变量,用set()命令。引用变量,用${}包裹变量名。就这么简单。
set(MY_VAR "Hello CMake")
message(STATUS "MY_VAR = ${MY_VAR}")
输出:
-- MY_VAR = Hello CMake
嗯,这里有个细节:message()里的${MY_VAR},CMake会在解析时把它替换成实际的值。这个替换发生在命令解析阶段,不是运行时。所以如果你写${${MY_VAR}}这种嵌套引用,也是可以的——先解析内层,再解析外层。
MYPROJ_SOURCES、MYPROJ_INCLUDE_DIRS。这样在大型项目里,一眼就能看出变量属于哪个模块。
3.2 set() 命令的多种形态
set()不只是用来设单个值的。它有好几种用法,我列个表给你看:
| 用法 | 示例 | 说明 |
|---|---|---|
| 普通变量 | set(VAR "value") |
最常用,设一个字符串 |
| 列表变量 | set(VAR a b c) |
传多个值,自动变成分号分隔的列表 |
| 缓存变量 | set(VAR "val" CACHE STRING "描述") |
会写入CMakeCache.txt,用户可修改 |
| 环境变量 | set(ENV{VAR} "val") |
设置系统环境变量(不常用) |
这里我要特别说一下列表变量。CMake里没有真正的数组,所谓的列表就是分号分隔的字符串。比如:
set(SOURCES main.cpp utils.cpp)
# 等价于 set(SOURCES "main.cpp;utils.cpp")
message(STATUS "${SOURCES}") # 输出 main.cpp;utils.cpp
你可能会问:那怎么遍历呢?用foreach()就行,后面章节会细讲。
set(SOURCES main.cpp utils.cpp)时,如果某个文件名包含空格,比如my file.cpp,CMake会把它当成两个值。解决办法是用双引号包起来:set(SOURCES "my file.cpp" utils.cpp)。
3.3 unset() 命令:把变量删干净
有定义就有删除。unset()用来移除一个变量。用法很简单:
set(TEMP_VAR "临时数据")
message("TEMP_VAR = ${TEMP_VAR}") # 有值
unset(TEMP_VAR)
message("TEMP_VAR = ${TEMP_VAR}") # 空字符串
注意:unset()之后,变量变成未定义状态。引用未定义的变量,CMake不会报错,而是返回空字符串。这一点和很多编程语言不同,要小心。
另外,unset()也可以用来删除缓存变量:
unset(MY_CACHE_VAR CACHE)
这个CACHE关键字很重要。不加的话,它只删除普通变量;加了,才删除缓存里的同名变量。我见过有人写unset(MY_VAR),结果缓存里还留着旧值,导致下次构建时行为诡异。
3.4 变量作用域:函数、目录、全局
这是变量最容易被忽视的地方。CMake的作用域分三层:
- 函数作用域:在
function()内部定义的变量,外部看不到。 - 目录作用域:每个
CMakeLists.txt文件有自己的作用域。子目录用add_subdirectory()引入时,会继承父目录的变量,但修改不会影响父目录。 - 全局作用域:用
set(VAR ... CACHE INTERNAL)或set(VAR ... PARENT_SCOPE)可以跨作用域传递。
我画个图帮你理解:
你看,根目录的变量可以传给子目录,但子目录改了也不会影响根目录。这其实是个好设计——每个模块自己管自己的变量,互不干扰。
但有时候,你确实需要在子目录里修改父目录的变量。比如子目录想往一个列表里追加源文件。这时候用PARENT_SCOPE:
# 在子目录的 CMakeLists.txt 中
set(MY_LIST "new_item" PARENT_SCOPE)
注意:PARENT_SCOPE会覆盖父目录的同名变量,而不是追加。如果你想追加,得先读取父变量的值:
# 子目录中
set(MY_LIST "${MY_LIST};new_item" PARENT_SCOPE)
set(SOURCES ...)定义了自己的源文件列表,结果父目录的SOURCES被意外覆盖了。排查了半天才发现是作用域问题。后来我养成了习惯:子目录的变量名加个前缀,比如SUBDIR_SOURCES。
3.5 变量引用机制:${} 的底层逻辑
${}看起来简单,但它的解析时机和规则,值得深究一下。
CMake解析${VAR}时,会做两件事:
- 查找名为
VAR的变量,如果没找到,返回空字符串。 - 如果找到了,把
${VAR}替换成变量的值。
这个替换发生在命令参数解析之前。所以你可以写出这样的代码:
set(CMD "message")
set(ARG "Hello")
${CMD}(${ARG}) # 等价于 message(Hello)
嗯,这种用法虽然灵活,但我建议少用。可读性太差了。
另外,${}可以嵌套:
set(KEY "name")
set(name "Alice")
message("${${KEY}}") # 输出 Alice
解析过程:先算内层${KEY}得到name,再算外层${name}得到Alice。
${${${VAR}}},是不是想骂人?
3.6 缓存变量 vs 普通变量
最后提一嘴缓存变量。普通变量只在当前CMake运行期间有效。缓存变量会写入CMakeCache.txt,下次构建时仍然存在。
定义缓存变量的标准写法:
set(MY_OPTION "ON" CACHE BOOL "是否启用某个功能")
这里BOOL是变量类型,CMake的缓存变量支持BOOL、STRING、FILEPATH、PATH、INTERNAL等类型。INTERNAL类型不会显示在cmake-gui中,适合内部使用。
缓存变量有个特点:如果CMakeCache.txt里已经存在同名变量,set()不会覆盖它。除非你加FORCE:
set(MY_OPTION "OFF" CACHE BOOL "描述" FORCE)
这个FORCE我一般只在初始配置时用,平时尽量让用户通过cmake-gui或命令行修改。
好了,变量基础就这些。下一章咱们聊聊列表操作和字符串处理,那才是真正让变量发挥威力的地方。