一聚教程网:一个值得你收藏的教程网站

最新下载

热门教程

C/C++开发中CMake语法深度解析之控制流、列表与函数实战指南

时间:2026-08-07 09:54:51 编辑:袖梨 来源:一聚教程网

C/C++开发中CMake语法深度解析之控制流、列表与函数实战指南并不只看表面做法,关键还要理解相关条件、限制和后续影响。

前言:

在 C/C++ 跨平台开发中,CMake 早已成为事实标准。但很多开发者仅停留在"复制粘贴模板代码"的阶段,当需要处理复杂的条件判断、批量文件操作或代码复用场景时,往往束手无策。控制流语句、列表操作和函数是 CMake 脚本的三大核心支柱,掌握它们才能真正从"模板搬运工"转变为"构建系统设计师"。下文会结合 CMake 源码逻辑和实战代码,从原理到实践逐行拆解 if 判断、foreach/while 循环、list 函数和自定义函数的所有用法,帮你彻底攻克 CMake 脚本编写的核心难点。

C/C++开发中CMake语法深度解析之控制流、列表与函数实战指南

一. if 判断:CMake 逻辑控制的基石

if() 是 CMake 中最基础也最容易出错的控制流语句。它的行为与大多数编程语言有显著差异,特别是在变量解析和布尔值判断方面,很多坑都源于对其底层逻辑的不了解。

1.1 基本语法结构

CMake 的 if 语句支持完整的条件分支结构:

if(<condition>)  <commands>elseif(<condition>) # 可选,可重复  <commands>else() # 可选  <commands>endif()

核心特性

  1. 支持嵌套使用
  2. 条件表达式无需括号包裹
  3. 多个条件可通过逻辑运算符组合
  4. 小括号 () 可用于改变运算优先级

1.2 布尔值判断的底层逻辑

CMake 中布尔值的判断规则非常特殊,我们可以从 CMake 源码中的 GetBooleanValue 函数一探究竟:

// CMake 源码片段:GetBooleanValue 函数逻辑bool GetBooleanValue(const std::string& arg) const {  // 第一步:检查命名常量  if (cmIsOn(arg)) return true;  // "1", "ON", "YES", "TRUE", "Y"  if (cmIsOff(arg)) return false; // "0", "OFF", "NO", "FALSE", "N", "IGNORE", "NOTFOUND"  // 第二步:检查是否为数字  char* end;  double d = std::strtod(arg.c_str(), &end);  if (*end == '') { // 整个字符串都是数字    return static_cast<bool>(d);  }  // 第三步:检查是否为已定义的变量  cmValue def = this->GetDefinitionIfUnquoted(arg);  return !def.IsOff();}

基于这个逻辑,我们可以总结出完整的布尔值判断规则:

输入类型判断为 true 的情况判断为 false 的情况
命名常量“1”, “ON”, “YES”, “TRUE”, “Y”(大小写不敏感)“0”, “OFF”, “NO”, “FALSE”, “N”, “IGNORE”, “NOTFOUND”,以及以 “-NOTFOUND” 结尾的字符串
数字非零数字(包括浮点数)0
变量变量已定义且值不是 false 常量变量未定义,或值为 false 常量
字符串字面量仅当字符串内容是 true 常量其他所有字符串

实战验证代码

cmake_minimum_required(VERSION 3.18)project(IfBooleanDemo)# 1. 命名常量测试message("=== 命名常量测试 ===")if("ON")        message("1: ON = true")    else() message("1: ON = false")    endif()if("OFF")       message("2: OFF = true")   else() message("2: OFF = false")   endif()if("YES")       message("3: YES = true")   else() message("3: YES = false")   endif()if("NO")        message("4: NO = true")    else() message("4: NO = false")    endif()# 2. 数字测试message("n=== 数字测试 ===")if("1.1")       message("5: 1.1 = true")   else() message("5: 1.1 = false")   endif()if("0")         message("6: 0 = true")     else() message("6: 0 = false")     endif()if("123A")      message("7: 123A = true")  else() message("7: 123A = false")  endif()# 3. 变量测试message("n=== 变量测试 ===")set(OS "linux")if(OS)          message("8: OS(linux) = true")  else() message("8: OS(linux) = false")  endif()set(OS "OFF")if(OS)          message("9: OS(OFF) = true")   else() message("9: OS(OFF) = false")   endif()# 4. 字符串字面量测试message("n=== 字符串字面量测试 ===")if("abc")       message("10: "abc" = true")  else() message("10: "abc" = false")  endif()if("on")        message("11: "on" = true")   else() message("11: "on" = false")   endif()

运行结果

=== 命名常量测试 ===

1: ON = true

2: OFF = false

3: YES = true

4: NO = false

=== 数字测试 ===

5: 1.1 = true

6: 0 = false

7: 123A = false

=== 变量测试 ===

8: OS(linux) = true

9: OS(OFF) = false

=== 字符串字面量测试 ===

10: "abc" = false

11: "on" = true

关键陷阱

  1. 普通字符串字面量(如"abc")永远为 false,除非它恰好是 true 常量
  2. 变量名不需要 ${} 包裹,if(OS) 等价于 if(${OS})
  3. 环境变量不能直接用 if(ENV{VAR}) 测试,必须先展开:if($ENV{VAR})

1.3 常用比较操作

CMake 提供了丰富的比较操作符,适用于不同类型的数据:

1.3.1 数值比较

  1. EQUAL:等于
  2. LESS:小于
  3. GREATER:大于
  4. LESS_EQUAL:小于等于
  5. GREATER_EQUAL:大于等于
if("20" LESS "30")  message("20 < 30 = true")endif()

1.3.2 字符串比较

  1. STREQUAL:等于
  2. STRLESS:字典序小于
  3. STRGREATER:字典序大于
if("abc" STRLESS "abd")  message("abc < abd (字典序) = true")endif()

1.3.3 版本号比较

  1. VERSION_EQUAL:版本等于
  2. VERSION_LESS:版本小于
  3. VERSION_GREATER:版本大于
set(VERSION "1.2.3")if(VERSION VERSION_LESS "1.3.0")  message("版本 1.2.3 < 1.3.0 = true")endif()

1.3.4 路径比较

  1. PATH_EQUAL:路径相等(CMake 3.24+)
if("/a/b/c" PATH_EQUAL "/a//b/c/")  message("路径相等 = true") # 多个分隔符会被合并endif()

1.4 逻辑运算与存在性检测

1.4.1 逻辑运算符

  1. NOT:逻辑非
  2. AND:逻辑与
  3. OR:逻辑或
if(NOT "OFF")  message("NOT OFF = true")endif()if("ON" AND "OFF")  message("ON AND OFF = true")else()  message("ON AND OFF = false")endif()if("ON" OR "OFF")  message("ON OR OFF = true")endif()

1.4.2 存在性检测

CMake 提供了多种检测命令,用于检查文件、目标、变量等是否存在:

检测命令功能
EXISTS <path>检查文件或目录是否存在
IS_DIRECTORY <path>检查是否为目录
IS_ABSOLUTE <path>检查是否为绝对路径
COMMAND <command>检查命令是否存在
TARGET <target>检查目标是否存在
DEFINED <var>检查变量是否定义
DEFINED CACHE{<var>}检查缓存变量是否定义
DEFINED ENV{<var>}检查环境变量是否定义

实战示例

# 检查文件是否存在if(EXISTS ${CMAKE_CURRENT_LIST_FILE})  message("当前 CMakeLists.txt 存在")endif()# 检查目标是否存在add_library(MyMath INTERFACE IMPORTED)if(TARGET MyMath)  message("目标 MyMath 存在")endif()# 检查变量是否定义set(NAME "bit")if(DEFINED NAME)  message("变量 NAME 已定义")endif()# 检查环境变量是否定义if(DEFINED ENV{PATH})  message("环境变量 PATH 已定义")endif()

二. 循环:批量处理的利器

CMake 提供了两种循环结构:foreachwhile。其中 foreach 用于遍历列表或范围,是最常用的循环方式;while 用于在条件为真时重复执行代码块。

2.1 foreach 循环的五种语法形式

foreach 循环语法非常灵活,共有五种常用形式:

2.1.1 基本形式:直接列举元素

foreach(<variable> <item1> <item2> ...)  # 循环体endforeach()

示例

foreach(X "1" "2;3;4" "abc")  message("X1: ${X}")endforeach()

输出

X1: 1X1: 2;3;4X1: abc

注意:分号分隔的字符串会被当作一个整体元素。

2.1.2 RANGE 形式:遍历数字范围

# 遍历 0 到 stop(包含)foreach(<variable> RANGE <stop>)# 遍历 start 到 stop(包含),步长为 stepforeach(<variable> RANGE <start> <stop> [<step>])

示例

# 遍历 0 到 10foreach(X RANGE 10)  message("X2: ${X}")endforeach()# 遍历 10 到 20,步长为 3foreach(X RANGE 10 20 3)  message("X3: ${X}")endforeach()

输出

X2: 0

X2: 1

...

X2: 10

X3: 10

X3: 13

X3: 16

X3: 19

2.1.3 IN ITEMS 形式:遍历字面量列表

foreach(<variable> IN ITEMS <item1> <item2> ...)

与基本形式类似,但更明确地表示后面是字面量元素:

foreach(X IN ITEMS "1" "2;3;4" "abc")  message("X4: ${X}")endforeach()

输出与基本形式完全相同。

2.1.4 IN LISTS 形式:遍历变量列表

foreach(<variable> IN LISTS <list_variable1> <list_variable2> ...)

这是最常用的形式,用于遍历存储在变量中的列表:

set(L1 "1")set(L2 "2;3;4")set(L3 "abc")foreach(X IN LISTS L1 L2 L3)  message("X5: ${X}")endforeach()

输出

X5: 1

X5: 2

X5: 3

X5: 4

X5: abc

关键区别IN LISTS 会自动展开变量中的列表(按分号拆分),而 IN ITEMS 不会。

2.2 while 循环

while 循环在条件为真时重复执行代码块:

while(<condition>)  # 循环体endwhile()

示例:计数器循环

set(COUNT 1)while(COUNT LESS_EQUAL 5)  message(STATUS "计数: ${COUNT}")  math(EXPR COUNT "${COUNT} + 1") # 递增计数endwhile()

输出

-- 计数: 1

-- 计数: 2

-- 计数: 3

-- 计数: 4

-- 计数: 5

注意事项

  1. 必须在循环体内修改条件变量,否则会导致无限循环
  2. 使用 math(EXPR) 进行数值计算
  3. while 循环的条件判断规则与 if 完全相同

三. list 函数:列表操作的瑞士军刀

在 CMake 中,列表本质上是用分号分隔的字符串。list 函数提供了丰富的操作方法,用于处理这种特殊的数据结构。

3.1 基本语法

list(COMMAND <list_variable> [args...])

list 函数的第一个参数是操作命令,第二个参数是要操作的列表变量。

3.2 常用列表操作

3.2.1 添加元素

  1. APPEND:在列表末尾添加元素
  2. PREPEND:在列表开头添加元素
  3. INSERT:在指定位置插入元素
set(SRCS "main.cpp" "utils.cpp" "math.cpp")# 末尾添加list(APPEND SRCS "network.cpp")message("APPEND: ${SRCS}") # main.cpp;utils.cpp;math.cpp;network.cpp# 开头添加list(PREPEND SRCS "config.cpp")message("PREPEND: ${SRCS}") # config.cpp;main.cpp;utils.cpp;math.cpp;network.cpp# 指定位置插入(索引从 0 开始)list(INSERT SRCS 2 "logger.cpp")message("INSERT: ${SRCS}") # config.cpp;main.cpp;logger.cpp;utils.cpp;math.cpp;network.cpp

3.2.2 移除元素

  1. REMOVE_ITEM:移除指定值的元素
  2. REMOVE_AT:移除指定索引的元素
  3. REMOVE_DUPLICATES:移除重复元素
# 移除指定值list(REMOVE_ITEM SRCS "math.cpp")message("REMOVE_ITEM: ${SRCS}")# 移除指定索引list(REMOVE_AT SRCS 1)message("REMOVE_AT: ${SRCS}")# 移除重复list(APPEND SRCS "utils.cpp")list(REMOVE_DUPLICATES SRCS)message("REMOVE_DUPLICATES: ${SRCS}")

3.2.3 其他常用操作

  1. LENGTH:获取列表长度
  2. GET:获取指定索引的元素
  3. SORT:排序列表
  4. REVERSE:反转列表
  5. JOIN:用指定字符串连接列表元素
# 获取长度list(LENGTH SRCS LEN)message("列表长度: ${LEN}")# 获取元素list(GET SRCS 0 FIRST_ITEM)message("第一个元素: ${FIRST_ITEM}")# 排序list(SORT SRCS)message("排序后: ${SRCS}")# 反转list(REVERSE SRCS)message("反转后: ${SRCS}")# 连接list(JOIN SRCS " | " JOINED)message("连接后: ${JOINED}")

3.3 列表操作的常见陷阱

  1. 空列表处理:空列表在 CMake 中是空字符串,很多操作对空列表无效
  2. 元素包含分号:如果元素本身包含分号,需要用引号包裹
  3. 列表展开:在函数参数中传递列表时,会自动展开为多个参数

四. 函数:代码复用与模块化的核心

函数是组织 CMake 代码、提高复用性的重要工具。通过定义函数,可以封装复杂的操作,避免代码重复,使构建脚本更加清晰易读。

4.1 函数的定义与调用

# 定义函数function(<function_name> <arg1> <arg2> ...)  # 函数体endfunction()# 调用函数<function_name>(<value1> <value2> ...)

示例:简单的加法函数

function(add_numbers A B)  math(EXPR RESULT "${A} + ${B}")  message("${A} + ${B} = ${RESULT}")endfunction()# 调用add_numbers(3 5) # 输出: 3 + 5 = 8

4.2 参数传递:内置变量详解

CMake 函数提供了一组内置变量,用于访问传递给函数的参数:

内置变量描述示例(调用 func(a b c d e))
${ARGC}参数总数5
${ARGV}所有参数组成的列表“a;b;c;d;e”
${ARGV0}第一个参数“a”
${ARGV1}第二个参数“b”
${ARGN}剩余参数(可变参数)若函数定义为 func(a b),则为 “c;d;e”

实战示例:打印所有参数

function(print_args first second)  message("ARGC = ${ARGC}")  message("ARGV = ${ARGV}")  message("ARGV0 = ${ARGV0}")  message("ARGV1 = ${ARGV1}")  message("ARGV2 = ${ARGV2}")  message("ARGN = ${ARGN}")endfunction()# 调用print_args(A B C D E)

输出

ARGC = 5

ARGV = A;B;C;D;E

ARGV0 = A

ARGV1 = B

ARGV2 = C

ARGN = C;D;E

4.3 变量作用域:最容易踩坑的地方

CMake 函数具有独立的作用域,函数内部定义的变量默认是局部变量,不会影响外部作用域。

4.3.1 基本作用域规则

set(SCOP "全局作用域")function(print_var)  # 可以读取父作用域的变量  message("函数内读取: ${SCOP}")  # 函数内部修改变量,默认只影响局部作用域  set(SCOP "函数作用域")  message("函数内修改后: ${SCOP}")endfunction()print_var()# 外部变量的值不变message("函数外部: ${SCOP}")

输出

函数内读取: 全局作用域

函数内修改后: 函数作用域

函数外部: 全局作用域

4.3.2 PARENT_SCOPE 选项

如果需要将函数内部的变量值传递到父作用域,可以使用 PARENT_SCOPE 选项:

function(print_var)  message("函数内读取: ${SCOP}")  # 使用 PARENT_SCOPE 修改父作用域的变量  set(SCOP "函数作用域" PARENT_SCOPE)  # 注意:当前函数作用域的变量值仍然不变!  message("函数内修改后: ${SCOP}")endfunction()print_var()message("函数外部: ${SCOP}")

输出

函数内读取: 全局作用域

函数内修改后: 全局作用域 # 关键!当前作用域的值没有变

函数外部: 函数作用域 # 父作用域的值被修改了

底层原理

  1. 函数调用时,父作用域的变量会被拷贝到函数的临时内存空间
  2. PARENT_SCOPE 直接修改父作用域的原始内存空间
  3. 函数内部仍然使用临时内存空间的值

4.4 返回值的两种实现方式

CMake 函数没有传统的返回值语法,但可以通过以下两种方式"返回"结果:

方式 1:通过 PARENT_SCOPE 设置变量(推荐,如果你理解这个的话)

function(sum A B OUTPUT_VAR)  math(EXPR RESULT "${A} + ${B}")  set(${OUTPUT_VAR} ${RESULT} PARENT_SCOPE)endfunction()# 调用sum(3 5 RESULT)message("3 + 5 = ${RESULT}") # 输出: 3 + 5 = 8

方式 2:通过缓存变量(全局可见)

function(sum_global A B)  math(EXPR RESULT "${A} + ${B}")  set(GLOBAL_SUM ${RESULT} CACHE INTERNAL "全局求和结果" FORCE)endfunction()# 调用sum_global(3 5)message("全局结果: ${GLOBAL_SUM}") # 输出: 全局结果: 8

注意:缓存变量会跨多次 CMake 运行保持不变,使用时需谨慎。

总结:

1. if 判断

  1. 布尔值判断的三层逻辑:命名常量 → 数字 → 变量
  2. 普通字符串字面量(非 true 常量)永远为 false
  3. 环境变量必须用 if($ENV{VAR}) 测试,不能直接用 if(ENV{VAR})
  4. 掌握数值、字符串、版本号和路径四种比较操作

2. 循环

  1. foreach 的五种语法形式及适用场景
  2. IN LISTS 会展开变量中的列表,IN ITEMS 不会
  3. while 循环必须在体内修改条件变量,避免无限循环

3. list 函数

  1. 列表本质是分号分隔的字符串
  2. 掌握添加、移除、排序、获取长度等常用操作
  3. 注意空列表和包含分号的元素的特殊处理

4. 函数

  1. 参数传递的内置变量:ARGCARGVARGN
  2. 变量作用域规则:默认局部,PARENT_SCOPE 修改父作用域
  3. PARENT_SCOPE 的陷阱:当前函数作用域的值不变
  4. 返回值的两种实现方式:PARENT_SCOPE(推荐)和缓存变量

结尾:

热门栏目