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()
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}") # 这个变量确实存在
- 如果你需要封装逻辑、不希望变量泄露 → 用
function() - 如果你需要修改调用者的变量 → 用
macro() - 我个人的经验:90%的场景用函数就够了。宏容易造成变量污染,调试起来很头疼。
4.7 知识体系总览
下面这张图,把本章的核心知识点串起来了。你可以把它当成一个「语法速查地图」。
公众号:蓝海资料掘金营,微信deep3321