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}")
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 时随时回来瞄一眼。