3、CMake基础语法:注释、变量、命令调用、条件判断、循环、函数与宏的定义

说实话,很多初学者一上来就盯着 FetchContent 和 find_package 猛学,结果写出来的 CMakeLists.txt 跟意大利面条似的——能跑,但改起来想骂人。我个人习惯是,先把地基打牢。CMake 的语法其实不复杂,但有几个关键点你必须吃透。

这一章,我们就来聊聊 CMake 的“普通话”。掌握了这些,后面的模块化构建才能写得顺手。

3.1 注释:给未来的自己留个便签

注释这事儿,看着简单,但我在项目里见过太多“此地无银三百两”的注释了。CMake 支持三种注释方式:

  • 单行注释:用 # 开头,行内有效。
  • 行尾注释:命令后面跟 #,解释这行在干嘛。
  • 块注释:用 #[[ 和 ]# 包裹,适合临时屏蔽一大段逻辑。
# 这是一个单行注释
set(MY_VAR "hello")  # 行尾注释,说明这个变量的用途

#[[
  这是一个块注释
  可以用来临时注释掉整个函数或调试代码
  注意:嵌套使用时需要小心
]]#
我的习惯: 我一般只在“为什么这么做”的地方写注释,而不是“做了什么”。比如 # 因为旧版 GCC 不支持这个特性,所以手动降级,这种注释一年后回头看,你会感谢自己的。

3.2 变量:CMake 的“内存条”

CMake 的变量分两种:普通变量和缓存变量。说白了,普通变量是临时的,缓存变量是持久的(会写进 CMakeCache.txt)。

类型 作用域 设置方式 典型用途
普通变量 当前 CMakeLists.txt 及子目录 set(VAR value) 临时路径、局部标志
缓存变量 全局(跨构建目录) set(VAR value CACHE TYPE "描述") 用户可配置选项、安装路径
环境变量 系统环境 set(ENV{VAR} value) 读取 PATH、HOME 等
# 普通变量
set(MY_SRC_DIR "${CMAKE_SOURCE_DIR}/src")

# 缓存变量(用户可以在 cmake-gui 里修改)
set(MY_OPTION "ON" CACHE BOOL "是否启用高级功能")

# 引用变量
message(STATUS "源码目录: ${MY_SRC_DIR}")
我曾经踩过的坑: 在子目录的 CMakeLists.txt 里用 set(PARENT_SCOPE) 才能把变量传回父目录。如果你忘了加这个参数,变量只在当前作用域生效,父目录根本拿不到。排查了一下午才发现是这个问题。

3.3 命令调用:CMake 的“动词”

CMake 的命令调用格式很统一:命令名(参数1 参数2 ...)。参数之间用空格分隔,如果参数本身包含空格,必须用双引号包起来。

常用的命令就那么几个:

  • message():打印信息,调试利器。
  • set() / unset():设置/删除变量。
  • list():操作列表(追加、查找、排序等)。
  • file():文件操作(读写、下载、打包等)。
  • add_executable() / add_library():定义构建目标。
# 打印不同级别的信息
message(STATUS "当前构建类型: ${CMAKE_BUILD_TYPE}")
message(WARNING "这个选项已废弃,请使用新接口")
message(FATAL_ERROR "找不到必要的依赖库,构建终止")

# 列表操作
set(MY_LIST a b c)
list(APPEND MY_LIST d e)
list(LENGTH MY_LIST len)
message(STATUS "列表长度: ${len}")

核心要点: 命令名不区分大小写,但 CMake 社区惯例是全小写。参数中的变量引用用 ${},但 if() 判断中的变量名可以不加花括号(不过我个人习惯统一加,避免混淆)。

3.4 条件判断:让构建“长脑子”

条件判断是 CMake 里最常用的控制结构。你想想看,不同平台、不同编译器、不同选项,构建逻辑肯定不一样。

基本语法:

if(条件)
  # 条件为真时执行
elseif(另一个条件)
  # 另一个条件为真时执行
else()
  # 都不满足时执行
endif()

条件判断支持的操作符:

类别 操作符 示例
布尔 ON/OFF, TRUE/FALSE, YES/NO if(MY_OPTION)
字符串 STREQUAL, MATCHES if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
数字 EQUAL, LESS, GREATER if(CMAKE_CXX_STANDARD EQUAL 17)
文件 EXISTS, IS_DIRECTORY if(EXISTS "${MY_PATH}/config.h")
逻辑 AND, OR, NOT if(WIN32 AND NOT MINGW)
# 实际项目中的典型用法
if(CMAKE_SYSTEM_NAME STREQUAL "Windows")
  add_definitions(-DWIN32_LEAN_AND_MEAN)
elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux")
  target_link_libraries(my_target PRIVATE pthread)
elseif(APPLE)
  set(CMAKE_MACOSX_RPATH ON)
else()
  message(WARNING "未知平台,请手动配置")
endif()
避坑指南: 我曾经在 if() 里直接写 if(MY_VAR),结果 MY_VAR 的值是 "FALSE"(字符串),CMake 把它当成真值处理了。正确的做法是:if(MY_VAR STREQUAL "TRUE") 或者用 if(MY_VAR) 配合布尔缓存变量。记住,CMake 的 if() 对字符串的判断逻辑有点“反直觉”——非空字符串就是真。

3.5 循环:批量处理的艺术

CMake 的循环有两种:foreach() 和 while()。说实话,while() 我几乎没用过,因为 foreach() 已经能覆盖 99% 的场景了。

# 遍历列表
set(SOURCES main.cpp utils.cpp network.cpp)
foreach(SRC ${SOURCES})
  message(STATUS "处理源文件: ${SRC}")
  # 可以在这里做文件重命名、编译选项设置等操作
endforeach()

# 带范围的遍历
foreach(I RANGE 1 10)
  message(STATUS "迭代次数: ${I}")
endforeach()

# 遍历字典(CMake 3.10+)
foreach(key value IN ZIP_LISTS KEYS VALUES)
  message(STATUS "${key} -> ${value}")
endforeach()

实用技巧: 我在处理多个第三方库时,经常用 foreach() 配合 list() 来批量设置查找路径。比如:

set(LIB_NAMES openssl curl zlib)
foreach(LIB ${LIB_NAMES})
  find_library(${LIB}_LIBRARY ${LIB})
  if(NOT ${LIB}_LIBRARY)
    message(FATAL_ERROR "找不到 ${LIB} 库")
  endif()
endforeach()

3.6 函数与宏:代码复用的基石

函数和宏是 CMake 里实现模块化的核心。它们的区别很简单:

  • 函数(function):有自己的作用域,变量修改不影响外部(除非用 PARENT_SCOPE)。
  • 宏(macro):没有独立作用域,变量修改直接反映到调用者那里。

说白了,函数更安全,宏更“霸道”。我个人建议:能用函数就别用宏,除非你真的需要修改调用者的变量。

# 定义一个函数
function(add_sanitizer_flags target_name)
  if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang" OR CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
    target_compile_options(${target_name} PRIVATE -fsanitize=address -fno-omit-frame-pointer)
    target_link_options(${target_name} PRIVATE -fsanitize=address)
  elseif(MSVC)
    target_compile_options(${target_name} PRIVATE /fsanitize=address)
  endif()
endfunction()

# 调用函数
add_executable(my_app main.cpp)
add_sanitizer_flags(my_app)

# 定义一个宏(注意:宏的参数没有作用域隔离)
macro(print_variable var_name)
  message(STATUS "${var_name} = ${${var_name}}")
endmacro()

set(MY_VAR "hello")
print_variable(MY_VAR)  # 输出:MY_VAR = hello
我曾经踩过的坑: 在宏里用 return() 想提前退出,结果发现宏里的 return() 会直接跳出整个 CMakeLists.txt 的执行!因为宏本质上是代码替换,不是真正的函数调用。所以,如果你需要提前返回,请用函数而不是宏。

3.7 本章知识体系总览

下面这张图把本章的核心知识点串了起来。你可以把它当作一个“语法速查地图”,写 CMakeLists.txt 时随时回来瞄一眼。

CMake基础语法知识体系 CMake 基础语法知识体系 CMake 语法核心 注释 变量 命令调用 条件判断 循环 函数与宏 # 单行 / #[[ 块注释 ]] 普通 / 缓存 / 环境变量 message / set / list / file if / elseif / else / endif foreach / while / break function / macro / return 核心原则: 变量用 ${} 引用,函数优先于宏,条件判断注意字符串真假