CMake 系列教程(三):变量、条件与控制流
让你的构建脚本"聪明"起来
一、变量基础
1.1 定义与使用
# 定义普通变量 set(MY_NAME "CMake") set(MY_VERSION 3) # 使用变量:${变量名} message(STATUS "Project: ${MY_NAME}, Version: ${MY_VERSION}") # -- Project: CMake, Version: 3变量在 CMake 中本质是字符串,没有类型区分。
1.2 列表
CMake 通过分号;分隔实现列表:
# 两种等价写法 set(SOURCES a.cpp b.cpp c.cpp) # 空格分隔,自动转为分号 set(SOURCES "a.cpp;b.cpp;c.cpp") # 显式分号 # 结果相同:SOURCES = "a.cpp;b.cpp;c.cpp" # 使用列表 add_executable(myapp ${SOURCES}) # 展开为:add_executable(myapp a.cpp b.cpp c.cpp)列表操作
# 追加元素 list(APPEND SOURCES d.cpp e.cpp) # SOURCES = "a.cpp;b.cpp;c.cpp;d.cpp;e.cpp" # 在开头插入 list(INSERT SOURCES 0 main.cpp) # 删除元素 list(REMOVE_ITEM SOURCES c.cpp) # 获取长度 list(LENGTH SOURCES COUNT) message(STATUS "Source count: ${COUNT}") # 排序 list(SORT SOURCES)1.3 变量作用域
CMake 变量遵循函数作用域规则:
set(X "top-level") function(my_func) message(STATUS "Inside func, X = ${X}") # top-level(可读取外部变量) set(X "inside-func") # 仅在函数内修改,不影响外部 message(STATUS "After set, X = ${X}") # inside-func endfunction() my_func() message(STATUS "After func, X = ${X}") # top-level(函数内的修改未传播)从函数内部修改外部变量需要用PARENT_SCOPE:
function(my_func) set(X "modified" PARENT_SCOPE) # 修改调用者的 X endfunction()⚠️
add_subdirectory引入的子CMakeLists.txt也是一个新作用域,子目录修改的变量不会影响父目录(除非用PARENT_SCOPE)。
二、缓存变量
2.1 普通变量 vs 缓存变量
CMake 有两套独立的变量系统:
| 特性 | 普通变量 | 缓存变量 |
|---|---|---|
| 作用域 | 函数/目录作用域 | 全局持久 |
| 存储位置 | 内存 | CMakeCache.txt |
| 生命周期 | 配置阶段结束即消失 | 跨多次配置保留 |
| 设置方式 | set(VAR value) | set(VAR value CACHE TYPE "") |
| 优先级 | 高于缓存变量 | 低于普通变量 |
# 缓存变量 set(BUILD_TESTS ON CACHE BOOL "Whether to build tests") # 第一次配置:写入 CMakeCache.txt # 后续配置:不覆盖已有缓存值(除非 FORCE)2.2 缓存变量类型
| 类型 | 用途 | 在 cmake-gui 中的表现 |
|---|---|---|
BOOL | 开关 | 复选框 |
STRING | 字符串 | 文本框 |
FILEPATH | 文件路径 | 文件选择器 |
PATH | 目录路径 | 目录选择器 |
2.3 修改缓存变量
# 命令行方式cmake-Bbuild-DBUILD_TESTS=OFF# 交互式方式ccmake build/# 终端 TUIcmake-gui build/# 图形界面(Windows)2.4option命令
option是BOOL类型缓存变量的语法糖:
# 等价写法 option(BUILD_TESTS "Build test programs" ON) # set(BUILD_TESTS ON CACHE BOOL "Build test programs")💡
option一定要在project()之后调用,否则ON/OFF可能与缓存中的已有值冲突。
三、条件判断
3.1 基本语法
if(CONDITION) # ... elseif(ANOTHER_CONDITION) # ... else() # ... endif()3.2 常用条件表达式
布尔判断
# 以下值为"假":OFF, NO, FALSE, 0, N, IGNORE, NOTFOUND, 空字符串, 以 -NOTFOUND 结尾 # 其余为"真" if(BUILD_TESTS) message(STATUS "Tests enabled") endif()比较
# 数值比较 if(${PROJECT_VERSION_MAJOR} GREATER 2) # 字符串比较 if(CMAKE_SYSTEM_NAME STREQUAL "Linux") # 版本比较 if(CMAKE_VERSION VERSION_GREATER_EQUAL "3.20")| 操作符 | 含义 |
|---|---|
EQUAL/LESS/GREATER | 数值比较 |
STREQUAL/STRLESS/STRGREATER | 字符串比较 |
VERSION_EQUAL/VERSION_GREATER/VERSION_LESS | 版本号比较 |
逻辑组合
if(UNIX AND NOT APPLE) # Linux 环境 endif() if(WIN32 OR CYGWIN) # Windows 环境 endif()平台判断
if(WIN32) # Windows(含 64 位) if(UNIX) # Linux / macOS / BSD if(APPLE) # macOS / iOS if(MSVC) # Microsoft Visual C++ if(CMAKE_SIZEOF_VOID_P EQUAL 8) # 64 位 endif()文件系统
if(EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/config.h") message(STATUS "config.h found") endif() if(IS_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/include") message(STATUS "include directory exists") endif()3.3 常见陷阱
set(VAR "OFF") # ❌ 错误:永远为真,因为 "OFF" 是非空字符串 if(${VAR}) # ✅ 正确:展开后变成 if(OFF),进行布尔判断 if(${VAR}) # ✅ 更推荐:使用变量名,让 if 自动求值 if(VAR)💡最佳实践:在
if()中直接写变量名(不加${}),让 CMake 自动处理布尔语义。仅当需要字符串比较时才用${}。
四、循环
4.1foreach
# 遍历列表 set(LANGS C CXX CUDA) foreach(lang IN LISTS LANGS) message(STATUS "Language: ${lang}") endforeach() # 遍历值 foreach(i RANGE 1 5) # 1, 2, 3, 4, 5 message(STATUS "i = ${i}") endforeach() foreach(i RANGE 0 10 3) # 0, 3, 6, 9(步长为 3) message(STATUS "i = ${i}") endforeach() # 同时遍历多个列表 set(NAMES alpha beta gamma) set(VALUES 1 2 3) foreach(name val IN ZIP_LISTS NAMES VALUES) message(STATUS "${name} = ${val}") endforeach() # alpha = 1, beta = 2, gamma = 34.2while
set(COUNT 0) while(COUNT LESS 5) math(EXPR COUNT "${COUNT} + 1") message(STATUS "Count: ${COUNT}") endwhile()4.3 循环控制
foreach(i RANGE 1 10) if(i EQUAL 5) continue() # 跳过本次迭代 endif() if(i EQUAL 8) break() # 跳出循环 endif() message(STATUS "i = ${i}") endforeach() # 输出:1, 2, 3, 4, 6, 7五、函数与宏
5.1function
function(add_my_library name) # ARGN:所有额外参数 # ARGC:参数总数 # ARGV:所有参数列表 # ARGV0, ARGV1, ...:按位置访问 message(STATUS "Creating library: ${name}") message(STATUS "Sources: ${ARGN}") add_library(${name} STATIC ${ARGN}) target_include_directories(${name} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR} ) target_compile_features(${name} PUBLIC cxx_std_17) endfunction() # 调用 add_my_library(math math/add.cpp math/sub.cpp) # 创建一个名为 math 的静态库函数内部是独立作用域,修改的变量默认不传播到外部。
5.2macro
macro(my_macro arg) # 宏是文本替换,不做作用域隔离 message(STATUS "Macro arg: ${arg}") endmacro()函数 vs 宏:
| 特性 | function | macro |
|---|---|---|
| 作用域 | 独立 | 调用者作用域 |
| 参数传递 | 值传递(副本) | 文本替换 |
return() | 跳出函数 | 跳出包含宏的整个函数 |
| 推荐度 | ✅ 优先使用 | 仅当需要修改调用者变量时 |
⚠️强烈建议:除非有特殊需求,一律使用 function,避免宏的隐式作用域问题。
六、configure_file:生成配置头文件
6.1 问题场景
代码中需要用到版本号、构建类型等信息,但不能硬编码——这些值在 CMake 配置阶段才能确定。
6.2 解决方案
config.h.in(模板文件):
#pragmaonce#definePROJECT_VERSION"@PROJECT_VERSION@"#definePROJECT_NAME"@PROJECT_NAME@"#cmakedefineENABLE_LOGGING#cmakedefine01HAVE_OPENSSL// 使用 configure 变量#defineDATA_DIR"@CMAKE_INSTALL_PREFIX@/share/@PROJECT_NAME@"CMakeLists.txt:
cmake_minimum_required(VERSION 3.20) project(MyApp VERSION 2.1.0 LANGUAGES CXX) option(ENABLE_LOGGING "Enable logging" ON) # 查找 OpenSSL(可选) find_package(OpenSSL) # 生成 config.h configure_file( ${CMAKE_CURRENT_SOURCE_DIR}/config.h.in ${CMAKE_CURRENT_BINARY_DIR}/config.h @ONLY # 只替换 @VAR@ 形式,不替换 ${VAR} 形式 ) # 使用生成的头文件 add_executable(myapp main.cpp) target_include_directories(myapp PRIVATE ${CMAKE_CURRENT_BINARY_DIR} # 包含生成的 config.h )生成的 config.h(假设 ENABLE_LOGGING=ON, OpenSSL 已安装):
#pragmaonce#definePROJECT_VERSION"2.1.0"#definePROJECT_NAME"MyApp"#defineENABLE_LOGGING#defineHAVE_OPENSSL1#defineDATA_DIR"/usr/local/share/MyApp"6.3#cmakedefine规则
| 模板写法 | 变量为真 | 变量为假 |
|---|---|---|
#cmakedefine VAR | #define VAR | /* #undef VAR */ |
#cmakedefine01 VAR | #define VAR 1 | #define VAR 0 |
七、实用模式
7.1 多配置构建类型判断
# 兼容单配置和多配置生成器 get_property(isMultiConfig GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) if(isMultiConfig) # Visual Studio / Ninja Multi-Config message(STATUS "Multi-config generator") else() # Makefile / Ninja (单配置) if(NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type" FORCE) endif() endif()7.2 平台适配编译选项
function(set_default_compile_options target) target_compile_features(${target} PUBLIC cxx_std_17) if(MSVC) target_compile_options(${target} PRIVATE /W4 /utf-8) else() target_compile_options(${target} PRIVATE -Wall -Wextra -Wpedantic -Werror ) endif() endfunction() # 使用 add_executable(myapp main.cpp) set_default_compile_options(myapp)7.3 条件编译源文件
set(APP_SOURCES main.cpp app.cpp) if(WIN32) list(APPEND APP_SOURCES platform/win.cpp) elseif(UNIX AND NOT APPLE) list(APPEND APP_SOURCES platform/linux.cpp) elseif(APPLE) list(APPEND APP_SOURCES platform/macos.cpp) endif() add_executable(myapp ${APP_SOURCES})小结
| 知识点 | 要点 |
|---|---|
| 变量 | 字符串本质,${}引用,函数作用域 |
| 列表 | 分号分隔,list()操作 |
| 缓存变量 | CACHE类型,CMakeCache.txt,option |
| 条件 | if/elseif/else/endif,推荐变量名不加${} |
| 循环 | foreach为主,RANGE、ZIP_LISTS |
| 函数 | 独立作用域,优先于宏 |
configure_file | 模板生成配置头文件,#cmakedefine |
📖下一期预告:《CMake 系列教程(四):依赖管理》—— 从
find_package到FetchContent,解决 C/C++ 项目最头疼的第三方库集成问题。