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_SCOPEfunction(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 # 后续配置不覆盖已有缓存值除非 FORCE2.2 缓存变量类型类型用途在 cmake-gui 中的表现BOOL开关复选框STRING字符串文本框FILEPATH文件路径文件选择器PATH目录路径目录选择器2.3 修改缓存变量# 命令行方式cmake-Bbuild-DBUILD_TESTSOFF# 交互式方式ccmake build/# 终端 TUIcmake-gui build/# 图形界面Windows2.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.2whileset(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.1functionfunction(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.2macromacro(my_macro arg) # 宏是文本替换不做作用域隔离 message(STATUS Macro arg: ${arg}) endmacro()函数 vs 宏特性functionmacro作用域独立调用者作用域参数传递值传递副本文本替换return()跳出函数跳出包含宏的整个函数推荐度✅ 优先使用仅当需要修改调用者变量时⚠️强烈建议除非有特殊需求一律使用 function避免宏的隐式作用域问题。六、configure_file生成配置头文件6.1 问题场景代码中需要用到版本号、构建类型等信息但不能硬编码——这些值在 CMake 配置阶段才能确定。6.2 解决方案config.h.in模板文件#pragmaonce#definePROJECT_VERSIONPROJECT_VERSION#definePROJECT_NAMEPROJECT_NAME#cmakedefineENABLE_LOGGING#cmakedefine01HAVE_OPENSSL// 使用 configure 变量#defineDATA_DIRCMAKE_INSTALL_PREFIX/share/PROJECT_NAMECMakeLists.txtcmake_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_LOGGINGON, OpenSSL 已安装#pragmaonce#definePROJECT_VERSION2.1.0#definePROJECT_NAMEMyApp#defineENABLE_LOGGING#defineHAVE_OPENSSL1#defineDATA_DIR/usr/local/share/MyApp6.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.txtoption条件if/elseif/else/endif推荐变量名不加${}循环foreach为主RANGE、ZIP_LISTS函数独立作用域优先于宏configure_file模板生成配置头文件#cmakedefine下一期预告《CMake 系列教程四依赖管理》—— 从find_package到FetchContent解决 C/C 项目最头疼的第三方库集成问题。