4、CMake语法基础:注释、命令、变量、列表、流程控制(if/foreach/while)、函数与宏定义

好,咱们正式开始啃CMake的语法。说实话,很多初学者一上来就被CMakeLists.txt里那些花里胡哨的写法吓住了。其实你把它拆开看,核心语法就那么几块:怎么写字、怎么存东西、怎么判断、怎么循环、怎么封装。今天我就带你把这几个东西捋一遍。

4.1 注释:给代码留个话

注释这事,看着简单,但我在项目里见过不少「注释比代码还难懂」的案例。CMake支持两种注释方式:

# 这是单行注释,用井号开头
# 我习惯在关键逻辑前加一行注释,方便后来人看

#[[
  这是块注释
  可以跨越多行
  适合临时屏蔽一大段代码
#]]
我的习惯:块注释我很少用,因为容易嵌套出问题。单行注释更安全,也更容易跟代码对齐。

4.2 命令:CMake的「动词」

CMake里的命令,说白了就是函数调用。写法是:命令名(参数1 参数2 ...)。注意,参数之间用空格分隔,不是逗号。

# 最常用的几个命令
message("Hello CMake")          # 打印信息
add_executable(myapp main.cpp)  # 生成可执行文件
target_link_libraries(myapp PRIVATE fmt)  # 链接库

命令名不区分大小写,但我个人建议全小写。为什么?因为CMake官方文档里全是小写,你搜起来方便。

4.3 变量:存东西的盒子

CMake的变量,本质上都是字符串。你用set()来赋值,用${}来取值。

set(MY_NAME "CMake实战")   # 设置变量
message("项目名称: ${MY_NAME}")  # 取值打印

# 变量可以覆盖
set(MY_NAME "CMake进阶")
message("现在叫: ${MY_NAME}")
注意:如果你忘了加${},CMake会把变量名当成普通字符串处理。我曾经因为这个bug排查了半小时——打印出来全是变量名本身,而不是它的值。

CMake还有缓存变量,用set(... CACHE ...)来定义。缓存变量会写入CMakeCache.txt,用户可以通过cmake -D来修改。

set(BUILD_SHARED_LIBS ON CACHE BOOL "是否构建动态库")

4.4 列表:多个值放一起

CMake的列表,其实就是用分号分隔的字符串。你写set(LIST a b c),它内部存的是a;b;c

set(SOURCES main.cpp utils.cpp network.cpp)
message("源文件列表: ${SOURCES}")  # 输出: main.cpp;utils.cpp;network.cpp

# 列表操作
list(LENGTH SOURCES len)       # 获取长度
list(GET SOURCES 0 first)      # 获取第0个元素
list(APPEND SOURCES test.cpp)  # 追加元素
list(REMOVE_ITEM SOURCES utils.cpp)  # 删除元素

列表操作里有个坑:list(APPEND)set()追加的区别。前者是追加到列表末尾,后者是覆盖整个变量。我刚开始用的时候经常搞混。

操作 命令 结果
追加元素 list(APPEND L a b) L = "x;y;a;b"
覆盖变量 set(L a b) L = "a;b"

4.5 流程控制:让CMake「动脑子」

4.5.1 if/elseif/else:条件判断

CMake的if判断,语法跟其他语言差不多,但有几个坑要注意。

set(USE_OPENGL ON)

if(USE_OPENGL)
    message("启用OpenGL支持")
    find_package(OpenGL REQUIRED)
elseif(USE_VULKAN)
    message("启用Vulkan支持")
else()
    message("使用软件渲染")
endif()
判断规则:
  • if(ON)if(YES)if(TRUE)if(1) 都视为真
  • if(OFF)if(NO)if(FALSE)if(0) 都视为假
  • if(DEFINED var) 判断变量是否被定义
  • if(var STREQUAL "value") 字符串比较
  • if(var GREATER 10) 数值比较

我曾经犯过一个错:用if(WIN32)来判断是不是Windows。结果在64位Windows上,WIN32这个变量依然是真——它只表示「目标平台是Windows家族」,不区分32/64位。

4.5.2 foreach:遍历列表

foreach循环,我用的频率很高。尤其是处理多个源文件或者多个子目录时。

set(LIBS fmt spdlog json)

foreach(lib ${LIBS})
    message("正在查找库: ${lib}")
    find_package(${lib} REQUIRED)
endforeach()

# 还有范围遍历
foreach(i RANGE 1 5)
    message("计数: ${i}")
endforeach()

foreach有个变体叫foreach(... IN LISTS ...),可以同时遍历多个列表。不过说实话,我很少用,因为可读性不太好。

4.5.3 while:条件循环

while循环在CMake里用得不多,但偶尔有用。

set(counter 0)
while(counter LESS 5)
    message("counter = ${counter}")
    math(EXPR counter "${counter} + 1")
endwhile()
小心死循环:CMake的while没有break语句。如果你忘了更新循环条件,它会一直跑下去。我见过一个同事的脚本跑了10分钟还没结束——最后发现是忘记给计数器加1了。

4.6 函数与宏定义:封装你的逻辑

当你的CMakeLists.txt越来越长,你就需要把重复的逻辑封装起来。CMake提供了两种方式:function()macro()

4.6.1 function:真正的函数

函数会创建一个新的作用域。你在函数里设置的变量,外面看不到——除非你用PARENT_SCOPE

function(add_my_library name)
    add_library(${name} STATIC ${name}.cpp)
    target_include_directories(${name} PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
    message("已创建库: ${name}")
endfunction()

# 调用
add_my_library(utils)

函数参数通过${ARGV}${ARGC}${ARGN}来访问。ARGV是所有参数列表,ARGC是参数个数,ARGN是超出命名参数的部分。

function(print_args first)
    message("第一个参数: ${first}")
    message("所有参数: ${ARGV}")
    message("额外参数: ${ARGN}")
endfunction()

print_args(a b c d)
# 输出:
# 第一个参数: a
# 所有参数: a;b;c;d
# 额外参数: b;c;d

4.6.2 macro:简单的文本替换

宏跟函数最大的区别:宏不创建新作用域。你在宏里设置的变量,会影响到调用者的作用域。

macro(set_output_dir dir)
    set(OUTPUT_DIR ${dir})
    message("输出目录设为: ${OUTPUT_DIR}")
endmacro()

set_output_dir("build/release")
message("外部也能看到: ${OUTPUT_DIR}")  # 这个变量确实存在
函数 vs 宏:怎么选?
  • 如果你需要封装逻辑、不希望变量泄露 → 用function()
  • 如果你需要修改调用者的变量 → 用macro()
  • 我个人的经验:90%的场景用函数就够了。宏容易造成变量污染,调试起来很头疼。

4.7 知识体系总览

下面这张图,把本章的核心知识点串起来了。你可以把它当成一个「语法速查地图」。

CMake语法基础 · 知识体系 CMake语法 注释 命令 变量 列表 # 单行 / #[[ 块 ]] message / add_* / target_* set() / ${} / 缓存变量 list() 操作 流程控制 if / elseif / else foreach while 函数 function / 宏 macro 作用域 · 参数传递 · PARENT_SCOPE
学习建议:别试图一次性记住所有语法。你只需要知道「有这个东西」,用的时候回来查就行。我写了5年CMake,有些冷门语法还得翻文档。关键是理解核心概念:变量怎么传、作用域怎么划分、列表怎么操作。这三样搞懂了,剩下的都是查表的事。

公众号:蓝海资料掘金营,微信deep3321