6、CMake变量与缓存:变量作用域、缓存变量(set CACHE)、CMake预设变量、option()与条件编译

CMake的变量系统,说实话,是很多初学者最容易踩坑的地方。我自己刚接触CMake那会儿,就被变量作用域搞得晕头转向——明明在顶层设了个变量,进到子目录怎么就没了?后来才明白,CMake的变量不是全局的,它有自己的“地盘”概念。

6.1 变量作用域:每个目录都有自己的小世界

CMake的变量作用域,说白了就是“父传子,子不传父”。每个add_subdirectory()调用,都会创建一个新的作用域。子目录能看见父目录的变量,但反过来不行。

# 顶层 CMakeLists.txt
set(MY_VAR "hello")
add_subdirectory(sub)

# sub/CMakeLists.txt
message("MY_VAR = ${MY_VAR}")  # 输出:hello
set(MY_VAR "world")            # 修改的是子作用域的副本
# 回到顶层,MY_VAR 仍然是 "hello"

嗯,这里要注意:set()默认只修改当前作用域。如果你想让变量穿透到子目录,可以用PARENT_SCOPE关键字。我在项目中遇到过这样一个坑:在子目录里改了变量,以为顶层也能用,结果编译出来全是错的。排查了半天才发现是作用域问题。

我的习惯:在子目录中尽量不修改父作用域的变量。如果非要改,显式加上 PARENT_SCOPE,并在注释里说明原因。

6.2 缓存变量:让设置“记住”

普通变量只在CMake运行期间有效。下次运行cmake ..,一切归零。缓存变量就不一样了——它会被写入CMakeCache.txt,下次构建时自动加载。

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

缓存变量的典型用法是让用户通过cmake -D来覆盖默认值。比如:

cmake -DMY_CACHE_VAR="custom" ..

我个人建议:只有那些需要用户配置的、跨构建保留的变量,才用CACHE。内部临时变量别用缓存,否则CMakeCache.txt会变得又臭又长。

我曾经踩过的坑:在同一个变量上多次调用 set(... CACHE ...),如果不加 FORCE,第二次调用不会覆盖第一次的值。因为缓存变量一旦设置,就“赖着不走”了。

6.3 CMake预设变量:系统帮你准备好了

CMake内置了很多变量,直接拿来用就行。我整理了一份常用的:

变量名 含义 示例值
CMAKE_SOURCE_DIR 顶层源码目录 /home/user/project
CMAKE_BINARY_DIR 构建目录 /home/user/project/build
CMAKE_CURRENT_SOURCE_DIR 当前处理的源码目录 /home/user/project/src
CMAKE_CURRENT_BINARY_DIR 当前处理的构建目录 /home/user/project/build/src
PROJECT_NAME 当前项目名称 MyProject
PROJECT_SOURCE_DIR 当前项目的源码目录 /home/user/project
PROJECT_BINARY_DIR 当前项目的构建目录 /home/user/project/build
CMAKE_BUILD_TYPE 构建类型 Debug, Release
CMAKE_CXX_COMPILER C++编译器路径 /usr/bin/g++
CMAKE_CXX_STANDARD C++标准版本 17, 20

你想想看,如果没有这些预设变量,每次都要自己写路径,那得多痛苦。我个人最常用的是CMAKE_CURRENT_SOURCE_DIRCMAKE_CURRENT_BINARY_DIR,尤其是在处理子项目时,它们能帮你精确定位文件位置。

6.4 option()与条件编译:让构建灵活起来

option()本质上就是一个布尔型的缓存变量。它的典型用法是让用户选择是否启用某个功能:

option(USE_OPENCV "是否启用OpenCV支持" ON)

if(USE_OPENCV)
  find_package(OpenCV REQUIRED)
  target_link_libraries(myapp PRIVATE ${OpenCV_LIBS})
  target_compile_definitions(myapp PRIVATE USE_OPENCV=1)
endif()

用户可以通过-DUSE_OPENCV=OFF来关闭它。我在项目中经常用这种方式做功能开关——比如调试日志、性能分析、实验性特性等。

条件编译的常见模式:
  • 功能开关:option() + target_compile_definitions() 在代码中控制 #ifdef
  • 平台判断:WIN32, APPLE, UNIX 等预设变量
  • 编译器判断:CMAKE_CXX_COMPILER_ID 区分 GCC、Clang、MSVC

举个例子,我曾经需要同时支持Linux和Windows的串口通信:

if(WIN32)
  target_sources(myapp PRIVATE serial_win.cpp)
else()
  target_sources(myapp PRINARY serial_posix.cpp)
endif()

这样代码里就不用写一堆#ifdef _WIN32了,构建系统层面就帮你分好了。

知识体系总览

下面这张图总结了CMake变量系统的核心脉络:

CMake变量系统核心脉络 CMake变量 普通变量 作用域:父传子,子不传父 PARENT_SCOPE 向上传递 缓存变量 写入 CMakeCache.txt,持久化 cmake -D 覆盖,FORCE 强制更新 预设变量 CMAKE_* / PROJECT_* 系列 option() 本质是布尔缓存变量 三者结合:普通变量做内部逻辑,缓存变量做用户配置,预设变量做环境适配

总结一下:普通变量管内部逻辑,缓存变量管用户配置,预设变量管环境适配。再加上option()做条件编译,你就能写出灵活又健壮的CMake脚本了。嗯,这些技巧我在实际项目中反复用过,确实能省不少事。

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