ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

【CMake】第2期 详解 CMake 多目录工程、变量作用域与核心语法

【CMake】第2期 详解 CMake 多目录工程、变量作用域与核心语法

开头

OK了,书接上回,在了解了基本的CMake语法后,我们这一期要来学习下CMake多目录,以及一些基本函数语法。我们直接开始。

一.多目录(嵌套CMake)

单文件CMakeLists.txt只适合微型 Demo,工程规模一旦扩大,所有配置塞在一个文件里会出现三个核心问题:

  1. 职责混乱,可维护性差:库代码、主程序、测试、示例的构建逻辑混在一起,修改一处容易影响其他模块
  2. 配置效率低:修改测试代码也会触发整个项目重新配置,大型项目配置耗时会非常明显
  3. 无法按需构建:测试、示例、基准测试属于可选内容,无法做到默认不构建、按需单独编译

工业级项目的标准解法是:按模块拆分目录,每个目录一个独立的CMakeLists.txt,顶层通过add_subdirectory引入子模块,实现模块解耦、职责分离、按需构建

1. 基本机制——add_subdirectory()

add_subdirectory(source_dir)的行为完全等价于「函数调用」:

  1. 暂停当前CMakeLists.txt的执行
  2. 进入指定子目录,完整执行其中的CMakeLists.txt
  3. 执行完毕后返回父目录,继续执行下一行代码

核心结论:目标(Target)是全局可见的,普通变量不是。
只要在前面的目录中用add_library/add_executable定义过目标,后面所有目录都可以直接链接使用;但普通变量有独立作用域,子目录里定义的变量父目录默认看不见

add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL] [SYSTEM])
参数作用典型使用场景
source_dir子源目录路径,必须包含CMakeLists.txt,路径相对当前源目录引入本地模块目录
binary_dir指定对应的构建目录,默认和源目录同名第三方源码目录重命名构建目录,避免命名冲突
EXCLUDE_FROM_ALL该目录下的目标默认不加入默认构建,只有显式指定目标名才会编译测试代码、示例代码、基准测试,减少日常构建耗时
SYSTEM该目录的头文件按系统头文件处理,自动屏蔽其中的警告引入第三方源码、不想处理第三方代码的编译警告时

2. 使用示例

如上图,现在我们的目录结构是这样的,每一个子目录里都对应有个CMakeLists.txt


注意:app/CMakeLists.txt里没有add_subdirectory指向src,但它能用math这个 target——因为add_subdirectory(src)已经在它之前执行过了。target 是全局的,变量不是

目标是全局的,变量不是

3.构建树会镜像

add_subdirectory会自动在构建目录中创建和源目录同名的子目录,形成「源树 - 构建树」一一对应的镜像结构,也就是 CMake 「三棵树」模型中的源树和构建树的对应关系:

  • 源目录my_lib/→ 构建目录build/my_lib/
  • 源目录app/→ 构建目录build/app/

二.变量体系

1.三大作用域与set基础用法

CMake 中所有变量本质都是字符串类型,但根据生命周期、作用范围、持久化能力,可以分为三类,分别对应set函数的不同写法

(1)目录作用域:普通全局变量

这是最常用的普通变量,由set(var value)直接定义,属于「目录级」作用域,也是我们日常写配置最常用的类型

核心规则

  1. 向下继承,向上隔离:
    • 父目录定义的普通变量,子目录(add_subdirectory引入)可以直接读取;
    • 子目录内修改同名变量,只会修改自己的「本地副本」,不会影响父目录的原始变量;
    • 方向是单向的:子能读父,父不能读子。
  2. 作用范围:当前 CMakeLists.txt 所在目录,及其所有子目录。
  3. 生命周期:仅在本次 CMake 配置过程中有效,不会写入缓存文件,重新配置会重置

对应 set 语法

通俗理解:父目录把变量「抄了一份」给子目录,子目录改自己的抄本,原件不会变

(2)函数作用域:局部变量

在function()自定义函数内部定义的变量,默认是函数局部作用域,和目录作用域相互独立。

核心规则

  1. 函数内部可以读取父作用域(调用函数的目录 / 上层函数)的变量;
  2. 函数内直接修改变量,只在函数内部生效,函数外部访问不到;
  3. 要把函数内的计算结果传回外层,必须加PARENT_SCOPE关键字

对应set语法


PARENT_SCOPE:向上一级传递变量

作用

把当前作用域的变量,写入上一级父作用域,专门用于「子目录→父目录」「函数→调用方」的结果回传。

两个关键注意点

  1. 只影响直接父级,不会逐层穿透
    孙目录设置的PARENT_SCOPE变量,只会传到直接父目录,不会自动传到祖父目录;要跨多层传递,必须每一层都加PARENT_SCOPE逐层上传。
  2. 当前作用域的值不变
    加了PARENT_SCOPE后,变量只会写到父作用域,当前子目录 / 函数里的该变量仍然是旧值(甚至未定义),这是最容易混淆的细节。

典型场景

  • 子目录编译完库,把库的路径、版本等信息传回父目录;
  • 自定义函数返回计算结果、配置结果

(3)Cache 缓存作用域:持久化全局变量

缓存变量是 CMake 最特殊的一类变量,它会持久化写入CMakeCache.txt文件,跨多次 CMake 配置会话保持有效,且整个工程所有目录都能访问。

核心规则

  1. 全局可见:整个工程的所有 CMakeLists.txt、所有子目录都可以读取;
  2. 持久化保存:首次配置写入CMakeCache.txt,之后重新运行 cmake 会先读取缓存,默认不会被 CMakeLists 里的新默认值覆盖;
  3. 优先级:命令行-D参数 > 缓存里的旧值 > CMakeLists 里的默认值

对应set语法

set(VAR_NAME value CACHE <TYPE> "变量说明文字")

其中TYPE是变量类型,常见的有:

  • STRING:普通字符串
  • BOOL:布尔值(取值 ON/OFF)
  • PATH:目录路径
  • FILEPATH:文件路径

补充语法:option()
option本质就是一个简化的 BOOL 型缓存变量:

缓存变量的经典踩坑

修改了 CMakeLists 里的option默认值,重新构建发现没生效。

  • 原因:第一次配置后,值已经写入缓存,后续配置会优先读取缓存里的旧值;
  • 解决:删除 build 目录重新配置,或者用-D命令行参数强制覆盖

FORCE:强制覆盖缓存变量

作用

强制修改已经存在的缓存变量的值,打破「缓存旧值优先」的默认规则。

语法

set(VAR_NAME newValue CACHE STRING "变量说明" FORCE)

使用原则

  • 非常不推荐滥用:缓存变量的设计初衷,是让用户通过-D参数自由配置工程,代码里强制覆盖会破坏用户的配置权;
  • 仅用于极少数必须强制锁定配置的场景,比如某些编译选项、特性开关必须强制开启 / 关闭

2.内置变量

(1)路径类变量

这类变量全部由 CMake 自动维护,用来定位源码和构建目录,是编写所有路径的基础

变量名含义核心区别
CMAKE_SOURCE_DIR整个工程最顶层的源目录(根 CMakeLists.txt 所在目录)整个工程唯一,永远指向工程根目录
CMAKE_BINARY_DIR整个工程最顶层的构建目录对应根目录的构建路径
PROJECT_SOURCE_DIR最近一次project()命令所在的源目录子项目调用project()后值会变化;写库工程优先用这个,兼容性更好
PROJECT_BINARY_DIR最近一次project()对应的构建目录对应当前项目的构建路径
CMAKE_CURRENT_SOURCE_DIR当前正在处理的 CMakeLists.txt所在的源目录进入子目录后会自动切换,写相对路径最常用
CMAKE_CURRENT_BINARY_DIR当前 CMakeLists.txt 对应的构建目录存放生成文件、中间产物的默认位置
CMAKE_CURRENT_LIST_DIR当前正在执行的 cmake 文件所在目录和include()配合时与上面不同:引入子脚本时,CURRENT_SOURCE_DIR不变,CURRENT_LIST_DIR会变成脚本所在目录

(2)产物输出路径

用来控制可执行文件、库文件的输出位置,替代默认的「当前构建目录」

变量名作用对应产物类型
CMAKE_RUNTIME_OUTPUT_DIRECTORY可执行文件的统一输出目录可执行程序(Windows 下的 dll 也在此目录)
CMAKE_LIBRARY_OUTPUT_DIRECTORY动态库的统一输出目录Linux/macOS 的 .so/.dylib 动态库
CMAKE_ARCHIVE_OUTPUT_DIRECTORY静态库的统一输出目录.a/.lib 静态库

注意点

  1. 这三个是全局变量,必须在add_executable/add_library之前设置,才会对后续创
  2. 也可以针对单个目标设置属性,优先级高于全局变量:
set_target_properties(myapp PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)
  1. 关于旧写法EXECUTABLE_OUTPUT_PATH/LIBRARY_OUTPUT_PATH:
    这是 CMake 早期的遗留变量,现在已经不推荐使用,优先用上面带OUTPUT_DIRECTORY的标准变量,语义更清晰,支持更完善

(3)项目信息类

全部由project()命令自动生成,用来获取项目的基本信息

变量名含义
PROJECT_NAME当前项目的名称
PROJECT_VERSION完整版本号(如 1.2.3)
PROJECT_VERSION_MAJOR主版本号(对应 1.2.3 里的 1)
PROJECT_VERSION_MINOR次版本号(对应 1.2.3 里的 2)
PROJECT_VERSION_PATCH修订号(对应 1.2.3 里的 3)

(4)构建配置类

控制编译、链接的全局行为,通常通过命令行-D参数设置

变量名作用常见值
CMAKE_BUILD_TYPE构建类型Debug / Release / RelWithDebInfo / MinSizeRel
CMAKE_CXX_STANDARDC++ 语言标准11 / 14 / 17 / 20
CMAKE_CXX_COMPILERC++ 编译器路径如/usr/bin/g++
CMAKE_INSTALL_PREFIX安装前缀路径默认/usr/local

三.核心语法

1.include() 命令

include是 CMake 代码复用与模块化的核心命令,它会读取指定的.cmake脚本文件或 CMake 内置模块,将其中所有代码直接在当前作用域展开执行,等价于把文件内容完整复制粘贴到调用位置

(1)完整语法

include(<file|module> [OPTIONAL] [RESULT_VARIABLE <var>] [NO_POLICY_SCOPE])
参数详细说明
<file / module>两种输入形式:1.本地文件路径:直接指定.cmake文件的相对 / 绝对路径,相对路径默认基于当前源目录;2.模块名:直接写模块名(如GNUInstallDirs),CMake 会自动在官方模块路径中查找对应.cmake文件。
OPTIONAL可选标识。若文件 / 模块不存在,不会报错终止,仅静默跳过;不加该参数时找不到文件会直接配置失败。
RESULT_VARIABLE <var>将执行结果存入指定变量:找到并执行成功,变量存储文件的绝对路径;失败则存储NOTFOUND。

(2)两种典型使用方式

方式 1:引入本地自定义脚本
封装通用函数、统一编译配置,是项目内最常用的场景:

# 顶层 CMakeLists.txt 中引入项目自定义工具脚本 include(相对顶层CMake的相对路路径/XXXX.cmake) # 引入后可直接调用脚本中定义的函数/变量

方式 2:引入 CMake 内置模块

CMake 自带大量官方标准模块,直接写模块名即可引入,无需写路径和后缀:

# 引入安装目录标准模块,自动定义 CMAKE_INSTALL_LIBDIR 等标准路径变量 include(GNUInstallDirs) # 引入包配置辅助模块,用于生成 find_package 兼容的配置文件 include(CMakePackageConfigHelpers)

(3)include 与 add_subdirectory 的核心区别

对比维度includeadd_subdirectory
操作对象单个.cmake脚本文件包含 CMakeLists.txt 的工程目录
作用域不创建新作用域,在当前作用域直接执行创建独立子作用域,子目录变量默认不会回传父目录
构建目录不会在 build 目录下生成镜像子目录自动在构建树中创建对应子目录,存放编译产物
执行逻辑等价于代码文本复制粘贴等价于函数调用,进入子目录执行完再返回
典型用途封装通用函数、统一编译配置、工具链划分工程模块、分目录管理源码

路径关键细节

在include定义的函数中,两个极易混淆的路径变量:

  • CMAKE_CURRENT_SOURCE_DIR:调用时所在的当前源目录(随调用位置变化)
  • CMAKE_CURRENT_LIST_DIR:原本.cmake 文件所在的目录(固定不变)

编写通用脚本时,引用脚本同目录的资源必须用CMAKE_CURRENT_LIST_DIR,否则换目录调用会路径错误

2.add_defintions() 编译宏定义

add_definitions用于向当前目录及所有子目录的编译目标,添加C/C++ 预处理宏定义,对应编译器的-D(GCC/Clang)或/D(MSVC)参数,常用于条件编译、功能开关、版本号注入等场景

(1)基本用法

add_definitions(-DMACRO1 -DMACRO2=value ...)
  • 多个宏定义可以一次性写入,用空格分隔。
  • 格式为-D宏名或-D宏名=值,和编译器参数格式一致。
  • 不带值的宏默认定义为1

常用示例

  1. 定义功能开关宏
# 定义 DEBUG_LOG 宏,代码中可用 #ifdef DEBUG_LOG 判断 add_definitions(-DDEBUG_LOG)
  1. 定义带值的宏
# 注入项目版本号,代码中可直接使用 VERSION_STR 字符串 add_definitions(-DVERSION_STR="1.2.0" -DMAX_BUFFER_SIZE=1024)

作用域规则

add_definitions是目录级别的命令,作用域规则如下:

  1. 调用后,当前目录下的所有编译目标(add_executable、add_library)都会继承这些宏定义。
  2. 后续通过add_subdirectory进入的子目录,也会自动继承父目录的宏定义。
  3. 子目录中再次调用add_definitions,是追加效果,不会覆盖父目录的定义

(2)与 target_compile_definitions 的对比

target_compile_definitions()语法:

target_compild_definitions(<目标> PUBLIC/PRIVATE/INTERFACE 宏 = 值)
对比维度add_definitionstarget_compile_definitions
作用域目录级,影响当前 + 所有子目录所有目标目标级,仅影响指定的单个目标
粒度粗,无法针对单个目标差异化配置细,可精准控制每个目标的宏定义
传递性自动向下传递给子目录可通过PUBLIC/PRIVATE/INTERFACE精确控制传递性
推荐程度旧写法,不推荐新项目使用✅ 现代 CMake 标准写法

示例:

(3)注意事项与最佳实践

  1. 字符串值的引号转义:如果宏的值是字符串,CMake 中需要转义引号,否则会被解析成多个参数:
add_definitions(-DPROJECT_NAME=\"my_demo\")
  1. 避免全局污染:add_definitions会影响所有子目录,容易造成宏名冲突,新项目优先使用target_compile_definitions。
  2. 不要写-D前缀的误区:target_compile_definitions不需要写-D前缀,CMake 会自动添加;但add_definitions必须写-D。
  3. 删除宏定义:如果要移除已定义的宏,使用remove_definitions(-DMACRO_NAME),同样是目录级生效。

3.message() 消息输出命令

message是 CMake 最基础的调试与信息输出命令,用于在配置阶段向控制台打印文本、变量值、警告或错误信息,是排查作用域、变量值问题的核心工具。

语法

message([<级别>] "消息内容1" "消息内容2" ...)

多个字符串参数会被自动拼接成一条消息。

常用级别从低到高:

级别行为说明
STATUS普通状态信息,前缀带--,是最常用的级别,用于打印常规构建信息
WARNING警告信息,黄色高亮显示,不会终止配置流程
AUTHOR_WARNING面向项目开发者的警告,默认和 WARNING 行为一致,可通过开关抑制
SEND_ERROR报错信息,继续执行但配置最终失败,不会生成构建文件
FATAL_ERROR致命错误,立即终止配置与生成流程,用于不可恢复的错误

不写级别时,默认为普通消息,无前缀

示例

4.if 条件判断

if是 CMake 配置期逻辑控制的核心,所有判断都在配置阶段执行完毕,用于根据平台、编译器、用户选项等动态调整构建逻辑

(1)基本语法

if(<条件表达式>) # 条件为真时执行 elseif(<条件表达式>) # 上一个条件不满足、当前条件满足时执行 else() # 所有条件都不满足时执行 endif()

CMake 的控制语句必须成对闭合,if对应endif(),中间可以有零个或多个elseif,最多一个else

(2)判断类型

a.真假判断

只要展开后的最终字符串是下面任意一种 →假(FALSE)(匹配不区分大小写,必须完整精确匹配)

  1. 空字符串""
  2. 布尔假关键字:FALSE、OFF、NO、N
  3. 纯数字0(只有单独的0!00/0.0/-0都不算)
  4. NOTFOUND、IGNORE、以及以-NOTFOUND结尾的字符串(xxx-NOTFOUND)

✅其余全部 → 真(TRUE)

只要不在上面假列表里,非空,就是真。
例:ONYESY1-1"abc""0.0""00"" OFF "(带空格)全部为真


if( ... )括号里面写不同东西,展开逻辑不一样,展开之后,再套上面那张真假表。

  1. if(VAR)不带${}:普通变量自动展开

流程:

  1. CMake 去找名字叫VAR的变量
    • ✅ 变量已经定义:取变量的值,作为最终字符串
    • ❌ 变量没有定义:最终字符串 = 空字符串""
  2. 拿最终字符串去匹配真假表

示例:

set(A OFF)if(A)# 展开得到"OFF"→ 在假表 → falseendif()set(B hello)if(B)# 展开得到"hello"→ 不在假表 → trueendif()if(C)# C没定义 → 空字符串 → falseendif()

⚠️ 重点:不会递归展开!
set(X Y)set(Y OFF)
if(X)展开得到"Y",字符串 Y 不在假表 → true,不会继续去找 Y 的值。

  1. if("$ENV{VAR}")先展开环境变量到字面串(推荐稳定写法)

$ENV{VAR}是变量展开语法,和${}一样,在 if 解析前就展开:

  1. 读取环境变量 VAR
    • 环境变量不存在 / 值为空:展开结果是空串""
    • 环境变量存在:展开结果是环境变量的文本
  2. 外面加了双引号:保证它是单个参数,不会被分号、空格拆分
  3. 结果字符串拿去匹配真假表
if("$ENV{PATH}")#PATH 环境变量存在且非空 → 展开为长字符串,不在假表 → trueendif()if("$ENV{MY_NO_EXIST}")# 环境变量不存在 → 展开为空串 → falseendif()
  1. if(ENV{VAR})if 内置 ENV {} 条件语法(CMake 3.1 + 支持)

这是if命令单独识别的特殊形式,不是 $ENV 展开!不要搞混
流程:

  1. if 识别出ENV{VAR},直接读取环境变量
    • 不存在 / 值为空 → 最终值是空串
    • 存在 → 最终值是环境变量内容
  2. 拿结果匹配真假表
if(ENV{PATH})# 等价行为和上面"$ENV{PATH}"一致,新版CMake可用endif()

❗ 坑点:很多老教程、旧 CMake 版本不支持 if (ENV {}),写了会被当成普通变量 ENV {PATH},变量不存在 → 空 → false,于是大家得出结论:环境变量 if 永远是假,这个结论是旧版本语法不支持导致的,不是现代 CMake 的规则。
✅ 稳妥兼容写法:永远优先用if("$ENV{VAR}")

  1. if("something")引号包裹字面字符串

流程:

  1. 不做任何变量、环境变量查找!引号里面是什么,最终字符串就是什么
  2. 直接拿去匹配真假表
if("OFF")# 字符串OFF → 假表 → falseendif()if("OFF123")# 不是OFF精确匹配 → trueendif()if("0")# 纯数字0→ falseendif()if("0.0")# 不是纯0→ trueendif()
  1. if(${VAR})${} 提前展开(非常容易翻车)

流程:

  1. ${VAR}优先展开成变量的值
  2. 把展开后的文本直接丢进 if 里面当作参数
  3. 再执行 if 的逻辑
    ❌ 大坑:如果变量值带空格、分号,展开后会变成多个参数,直接语法报错!
set(MY"hello world")if(${MY})# 展开后if(hello world)两个参数!语法错误!endif()

结论:尽量不要写if(${VAR});非要写,一定要包引号:if("${VAR}")


b.字符串比较

使用STREQUAL(相等)、STRLESS(小于)、STRGREATER(大于),建议变量加引号避免空值报错:

if("${CMAKE_BUILD_TYPE}"STREQUAL"Debug")message("当前为 Debug 构建模式")endif()

c.数值比较

使用EQUAL(等于)、LESS(小于)、GREATER(大于):

set(VERSION_MAJOR2)if(VERSION_MAJOR LESS3)message("主版本号低于 3")endif()

d. 逻辑组合

支持AND、OR、NOT,可用小括号改变运算优先级:

if(WIN32AND(CMAKE_CXX_COMPILER_ID STREQUAL"MSVC"))message("当前环境:Windows + MSVC 编译器")endif()

e.文件与路径判断
  • EXISTS path:路径(文件 / 目录)是否存在
  • IS_DIRECTORY path:路径是否为目录
  • IS_ABSOLUTE path:路径是否为绝对路径
if(EXISTS"${PROJECT_SOURCE_DIR}/third_party/lib/libfoo.a")link_directories(third_party/lib)endif()

f.版本号比较

使用VERSION_LESS、VERSION_GREATER、VERSION_EQUAL,专门用于语义化版本对比:

if(CMAKE_VERSION VERSION_LESS3.20)message(FATAL_ERROR"CMake 版本过低,需要 3.20 及以上版本")endif()

g. 正则匹配

MATCHES用于正则匹配,非常适合判断编译器、系统名等多值场景:

if(CMAKE_CXX_COMPILER_ID MATCHES"GNU|Clang")target_compile_options(app PRIVATE-Wall-Wextra)endif()

h.变量是否定义

DEFINED判断变量是否被定义过,注意不要加${}:

if(DEFINED MY_CUSTOM_FLAG) message("自定义标志已设置: ${MY_CUSTOM_FLAG}") endif()

5.list 列表操作

CMake 中的列表本质是用分号分隔的字符串,比如set(SRCS a.cpp b.cpp)等价于set(SRCS "a.cpp;b.cpp")。list命令专门用于操作这类列表数据

a.追加元素:APPEND

向列表末尾追加一个或多个元素,原地修改原列表:

list(APPEND <变量> <要追加的内容>)


b.删除指定元素:REMOVE_ITEM

删除列表中所有匹配的元素值:

list(REMOVE_ITEM <变量> <要删除的内容>)


c.获取列表长度:LENGTH
list(LENGTH <变量> <OutputVar>)


d.按下标取值:GET
list(GET <变量> <idex 下标 > <OutputVar>)


e.指定连接符

用指定分隔符把列表拼接成单个字符串:

list(JOIN <变量> <字符> <OutputVar>)


f.查找元素:FIND

查找元素在列表中的下标,找不到返回 -1:

list(FIND <变量> <要找的内容> <OutputVar>)


g.排序:SORT
list(SORT <变量> COMPARE <compare> CASE <case> ORDER <way>)

</compare/>

  • STRING:按照字典序 (默认)
  • FILE_BASENAME:按照路径
  • NATURAL:自然数排序

</case/>

  • SENSITIVE:关心大小写
  • INSENSITIVE:不关心

</way/>

  • DESCENDING:降序
  • ASCENDING:升序

6. 循环

(1)foreach()

foreach用于遍历列表、范围枚举,是批量处理多个目标、多个文件的核心工具,所有循环逻辑同样在配置期执行

形式 1:直接枚举项

最基础写法,逐个遍历列出的所有项:

foreach(target app test_util benchmark) target_compile_options(${target} PRIVATE -Wall) endforeach()

形式 2:遍历列表变量

使用IN LISTS遍历一个列表变量,是最推荐的写法,可避免展开歧义:

set(MODULES math util net) foreach(mod IN LISTS MODULES) add_subdirectory(${mod}) endforeach()

形式 3:范围循环

使用RANGE生成整数序列,格式为RANGE 起始 结束 [步长],步长默认为 1:

# 输出 1 2 3 4 5 foreach(i RANGE 1 5) message("i = ${i}") endforeach() # 步长为2,输出 0 2 4 6 8 foreach(i RANGE 0 8 2) message("i = ${i}") endforeach()

注意事项

  1. 循环变量只在循环体内有效,退出循环后恢复为原来的值。
  2. CMake 3.19+ 才支持break()和continue(),低版本无法直接跳出循环。
  3. 遍历列表时若修改列表本身,可能导致遍历异常,建议先拷贝列表再操作

(2)while()

while用于条件循环,适合循环次数不固定、靠条件终止的场景

基本语法

while(<条件表达式>) # 循环体 endwhile()

条件表达式的语法规则和if完全一致。

配合 math (EXPR) 使用

CMake 中不能直接写算术运算,必须通过math(EXPR)命令执行数值计算,因此while几乎总是和它搭配:

set(n 0) while(n LESS 3) message("n = ${n}") math(EXPR n "${n} + 1") endwhile()

7.function():函数

function是 CMake 最推荐的封装方式,具备独立作用域,行为和常规编程语言的函数一致,适合封装可复用的构建逻辑

(1)基本语法

function(<函数名> [形参1 形参2 ...]) # 函数体:任意 CMake 命令 endfunction()
  • 函数名全局唯一,调用时直接写函数名 + 参数即可执行。
  • endfunction()后可补写函数名(如endfunction(add_warnings)),属于可选写法,用于提升可读性

(2)参数机制

CMake 函数的参数分为命名形参和内置自动变量两类

a.命名形参

在函数定义时声明的参数,调用时按位置一一对应传入:


b.内置参数变量

函数体内自动存在以下特殊变量,无需声明即可使用:

变量名含义
ARGC实际传入的参数总个数
ARGV全部参数组成的列表
ARGN命名形参之外的多余参数(变长参数列表)
ARGV0、ARGV1…第 N 个位置的参数(下标从 0 开始)

(3)返回值

CMake 函数没有return 值的语法,所有返回值都通过写入调用者作用域的变量来实现,核心是PARENT_SCOPE:

return () 命令

函数内调用return()会立即终止当前函数,回到调用者作用域继续执行,不会影响调用者后续逻辑

结尾

OK了,这一期的CMake学习就到这里了,我们下期继续。如果对你的学习有所帮助,感谢你的点赞、收藏、关注,我主页里有更好康的呦!!

往期回顾

1.【CMake】第1期 从 CMake 初识到库的构建
2. 【MySQL】第4期 用户管理与 C 语言连接编程详解
3. 【MySQL】第3期 索引、事务与视图系统详解

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进