CMake 系列教程(三):变量、条件与控制流

CMake 系列教程(三):变量、条件与控制流

让你的构建脚本"聪明"起来


一、变量基础

1.1 定义与使用

cmake 复制代码
# 定义普通变量
set(MY_NAME "CMake")
set(MY_VERSION 3)

# 使用变量:${变量名}
message(STATUS "Project: ${MY_NAME}, Version: ${MY_VERSION}")
# -- Project: CMake, Version: 3

变量在 CMake 中本质是字符串,没有类型区分。

1.2 列表

CMake 通过分号 ; 分隔实现列表:

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)
列表操作
cmake 复制代码
# 追加元素
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 变量遵循函数作用域规则:

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

cmake 复制代码
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 "")
优先级 高于缓存变量 低于普通变量
cmake 复制代码
# 缓存变量
set(BUILD_TESTS ON CACHE BOOL "Whether to build tests")

# 第一次配置:写入 CMakeCache.txt
# 后续配置:不覆盖已有缓存值(除非 FORCE)

2.2 缓存变量类型

类型 用途 在 cmake-gui 中的表现
BOOL 开关 复选框
STRING 字符串 文本框
FILEPATH 文件路径 文件选择器
PATH 目录路径 目录选择器

2.3 修改缓存变量

bash 复制代码
# 命令行方式
cmake -B build -DBUILD_TESTS=OFF

# 交互式方式
ccmake build/          # 终端 TUI
cmake-gui build/       # 图形界面(Windows)

2.4 option 命令

optionBOOL 类型缓存变量的语法糖:

cmake 复制代码
# 等价写法
option(BUILD_TESTS "Build test programs" ON)
# set(BUILD_TESTS ON CACHE BOOL "Build test programs")

💡 option 一定要在 project() 之后调用,否则 ON/OFF 可能与缓存中的已有值冲突。


三、条件判断

3.1 基本语法

cmake 复制代码
if(CONDITION)
    # ...
elseif(ANOTHER_CONDITION)
    # ...
else()
    # ...
endif()

3.2 常用条件表达式

布尔判断
cmake 复制代码
# 以下值为"假":OFF, NO, FALSE, 0, N, IGNORE, NOTFOUND, 空字符串, 以 -NOTFOUND 结尾
# 其余为"真"

if(BUILD_TESTS)
    message(STATUS "Tests enabled")
endif()
比较
cmake 复制代码
# 数值比较
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 版本号比较
逻辑组合
cmake 复制代码
if(UNIX AND NOT APPLE)
    # Linux 环境
endif()

if(WIN32 OR CYGWIN)
    # Windows 环境
endif()
平台判断
cmake 复制代码
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()
文件系统
cmake 复制代码
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 常见陷阱

cmake 复制代码
set(VAR "OFF")

# ❌ 错误:永远为真,因为 "OFF" 是非空字符串
if(${VAR})

# ✅ 正确:展开后变成 if(OFF),进行布尔判断
if(${VAR})

# ✅ 更推荐:使用变量名,让 if 自动求值
if(VAR)

💡 最佳实践 :在 if() 中直接写变量名(不加 ${}),让 CMake 自动处理布尔语义。仅当需要字符串比较时才用 ${}


四、循环

4.1 foreach

cmake 复制代码
# 遍历列表
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 = 3

4.2 while

cmake 复制代码
set(COUNT 0)
while(COUNT LESS 5)
    math(EXPR COUNT "${COUNT} + 1")
    message(STATUS "Count: ${COUNT}")
endwhile()

4.3 循环控制

cmake 复制代码
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.1 function

cmake 复制代码
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.2 macro

cmake 复制代码
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(模板文件):

c 复制代码
#pragma once

#define PROJECT_VERSION "@PROJECT_VERSION@"
#define PROJECT_NAME "@PROJECT_NAME@"

#cmakedefine ENABLE_LOGGING
#cmakedefine01 HAVE_OPENSSL

// 使用 configure 变量
#define DATA_DIR "@CMAKE_INSTALL_PREFIX@/share/@PROJECT_NAME@"

CMakeLists.txt

cmake 复制代码
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 已安装):

c 复制代码
#pragma once

#define PROJECT_VERSION "2.1.0"
#define PROJECT_NAME "MyApp"

#define ENABLE_LOGGING
#define HAVE_OPENSSL 1

#define DATA_DIR "/usr/local/share/MyApp"

6.3 #cmakedefine 规则

模板写法 变量为真 变量为假
#cmakedefine VAR #define VAR /* #undef VAR */
#cmakedefine01 VAR #define VAR 1 #define VAR 0

七、实用模式

7.1 多配置构建类型判断

cmake 复制代码
# 兼容单配置和多配置生成器
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 平台适配编译选项

cmake 复制代码
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 条件编译源文件

cmake 复制代码
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 为主,RANGEZIP_LISTS
函数 独立作用域,优先于宏
configure_file 模板生成配置头文件,#cmakedefine

📖 下一期预告 :《CMake 系列教程(四):依赖管理》------ 从 find_packageFetchContent,解决 C/C++ 项目最头疼的第三方库集成问题。

相关推荐
杨了个杨89821 小时前
Dockerfile介绍及镜像制作
java·开发语言
云烟成雨TD1 小时前
Spring AI Alibaba 1.x 系列【75】分布式智能体
人工智能·分布式·spring
一条泥憨鱼1 小时前
苍穹外卖【day5|Redis与店铺营业状态设置】
java·后端·mybatis·苍穹外卖
swordbob1 小时前
prototype 注入到 singleton 里,prototype是否还是线程安全的
安全·spring·单例模式·原型模式
要开心吖ZSH1 小时前
AI医疗分诊与健康咨询助手agent开发——(2)让AI输出可控:结构化分诊与安全规则
java·ai·agent·健康医疗·spring ai
百事牛科技1 小时前
Word只打需要的部分:4种打印范围设置方法
windows·word
San813_LDD3 小时前
[C语言]《Dev-C++ 报错解决手册(Day0607 精华版)》
java·前端·javascript
Anastasiozzzz4 小时前
从有限状态机到智能体图:传统 FSM 与 Agent Graph的演进
java·人工智能·python·ai