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)可以跨作用域传递。

我画个图帮你理解:

全局作用域 (CMakeCache.txt) 根目录 CMakeLists.txt 变量: PROJECT_NAME, CMAKE_CXX_STANDARD 子目录通过 add_subdirectory() 继承 子目录 src/CMakeLists.txt 继承父目录变量 修改不影响父目录 可用 PARENT_SCOPE 回传 函数内部作用域 function() ... endfunction() 变量默认局部 用 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}时,会做两件事:

  1. 查找名为VAR的变量,如果没找到,返回空字符串。
  2. 如果找到了,把${VAR}替换成变量的值。

这个替换发生在命令参数解析之前。所以你可以写出这样的代码:

set(CMD "message")
set(ARG "Hello")
${CMD}(${ARG})  # 等价于 message(Hello)

嗯,这种用法虽然灵活,但我建议少用。可读性太差了。

另外,${}可以嵌套:

set(KEY "name")
set(name "Alice")
message("${${KEY}}")  # 输出 Alice

解析过程:先算内层${KEY}得到name,再算外层${name}得到Alice。

我的建议:嵌套引用在写通用宏或函数时很有用,比如根据变量名动态取值。但日常写CMakeLists.txt,尽量保持简单。你想想看,三个月后你自己回来看代码,看到${${${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或命令行修改。

好了,变量基础就这些。下一章咱们聊聊列表操作和字符串处理,那才是真正让变量发挥威力的地方。