CMake入门——进阶使用
CMake 进阶
在前面的学习中,我们已经掌握了 CMake 的基本使用方法。本文章将进一步深入学习 CMake 的进阶功能,包括函数定义、宏定义、文件操作以及交叉编译配置等内容。
1. 定义函数
CMake 提供了 function() 命令用于定义函数,基本语法如下:
function(<name> [arg1 [arg2 [arg3 ...]]])
command1(args ...)
command2(args ...)
...
endfunction(<name>)
其中 <name> 为函数名,arg1、arg2…为函数的形式参数。endfunction 括号中的 <name> 可写可不写,如果写了则必须与 function 括号中的 <name> 一致。调用函数的方式与使用命令一样。
1.1 基本使用方法
# 定义函数 xyz
function(xyz arg1 arg2)
message("${arg1} ${arg2}")
endfunction()
# 调用函数
xyz(Hello World)
打印结果:
Hello World
1.2 使用 return() 退出函数
在函数中可以使用 return() 命令退出函数,类似于 C 语言中的 return 语句。但需要注意的是,return() 并不能用于返回参数给调用者。
function(xyz)
message(Hello)
return() # 退出函数
message(World)
endfunction()
xyz()
执行结果只会打印 Hello,不会打印 World,说明 return() 执行后即退出了当前函数。
1.3 可变参函数
在 CMake 中,调用函数时实际传入的参数个数可以大于函数定义的参数个数(甚至函数定义时参数个数为 0),但实际传入的参数个数必须大于或等于函数定义的参数个数。例如:
function(xyz arg1)
message(${arg1})
endfunction()
xyz(Hello World China)
打印结果:
Hello
函数 xyz 定义时只有一个参数,但实际传入 3 个参数,这并不会报错,message() 会打印出第一个参数 Hello。
这种设计可用于实现可变参函数(类似于 C 语言中的可变参数函数)。要通过这些额外参数,需要借助函数内部变量。
1.4 函数的内部变量
function() 函数中可以使用以下内部变量:
| 内部变量 | 说明 |
|---|---|
ARGVX |
X 为数字(如 ARGV0、ARGV1、ARGV2…),表示函数的第 X 个参数 |
ARGV |
存放实际调用时传入的所有参数(多个参数时为参数列表) |
ARGN |
若定义时参数为 2 个,实际传入 4 个,则 ARGN 存放剩余 2 个参数(参数列表) |
ARGC |
实际传入的参数个数 |
示例:
function(xyz arg1 arg2)
message("ARGC: ${ARGC}")
message("ARGV: ${ARGV}")
message("ARGN: ${ARGN}")
message("ARGV0: ${ARGV0}")
message("ARGV1: ${ARGV1}")
# 循环打印出各个参数
set(i 0)
foreach(loop ${ARGV})
message("arg${i}: " ${loop})
math(EXPR i "${i} + 1")
endforeach()
endfunction()
xyz(A B C D E F G)
打印结果:
ARGC: 7
ARGV: A;B;C;D;E;F;G
ARGN: C;D;E;F;G
ARGV0: A
ARGV1: B
arg0: A
arg1: B
arg2: C
arg3: D
arg4: E
arg5: F
arg6: G
由此可见,函数定义了两个形参 arg1 和 arg2,实际传入了 7 个参数(A~G),因此 ARGC 为 7,ARGV 包含了所有传入的参数,ARGN 则存放了超出形参个数的剩余 5 个参数(C~G)。通过 ARGV0、ARGV1 等可以单独访问每一个参数,也可结合 foreach 循环遍历 ARGV 列表。
1.5 函数的作用域
通过 function() 定义的函数具有全局作用域。也就是说,父源码中定义的函数可以在子源码中调用,子源码中定义的函数也可以在父源码中调用。
示例 1:父源码中定义函数,子源码中调用
顶层 CMakeLists.txt:
cmake_minimum_required(VERSION 3.5)
project(HELLO VERSION 1.1.0)
function(xyz)
message("Hello World!")
endfunction()
add_subdirectory(hello)
hello/CMakeLists.txt:
message("这是子源码")
xyz() # 调用父源码中定义的 xyz() 函数
示例 2:子源码中定义函数,父源码中调用
顶层 CMakeLists.txt:
cmake_minimum_required(VERSION 3.5)
project(HELLO VERSION 1.1.0)
add_subdirectory(hello)
message("这是父源码")
xyz()
hello/CMakeLists.txt:
message("这是子源码")
function(xyz)
message("Hello World!")
endfunction()
两种方式均可正常调用,说明通过 function() 定义的函数是全局有效的,不局限于当前源码文件。
2. 宏定义
CMake 提供了 macro() 命令用于定义宏,从某种程度上来说,宏(macro)和函数(function)是类似的,都是创建一段有名字的代码以便后续调用,并且都可以传递参数。
macro(<name> [arg1 [arg2 [arg3 ...]]])
COMMAND1(ARGS ...)
COMMAND2(ARGS ...)
...
endmacro(<name>)
endmacro 括号中的 <name> 可写可不写,如果写了则必须与 macro 括号中的 <name> 一致。
宏同样支持 ARGVX(X 为数字)、ARGC、ARGV、ARGN 这些内部变量。
2.1 宏的基本使用
macro(XYZ arg1 arg2)
message("ARGC: ${ARGC}")
message("ARGV: ${ARGV}")
message("ARGN: ${ARGN}")
message("ARGV0: ${ARGV0}")
message("ARGV1: ${ARGV1}")
set(i 0)
foreach(loop ${ARGV})
message("arg${i}: " ${loop})
math(EXPR i "${i} + 1")
endforeach()
endmacro()
XYZ(A B C D E)
打印结果:
ARGC: 5
ARGV: A;B;C;D;E
ARGN: C;D;E
ARGV0: A
ARGV1: B
arg0: A
arg1: B
arg2: C
arg3: D
arg4: E
2.2 宏与函数的区别
虽然宏和函数在定义上看起来几乎一样,但它们之间存在重要区别:
区别一:宏的参数和内部变量是字符串替换,而非真正的变量
在宏定义中,宏的参数以及 ARGC、ARGV、ARGN 等值并不是真正的 CMake 变量,而是字符串替换。当 CMake 执行宏时,会先将这些标识符替换为对应的字符串值,然后再执行宏代码——类似于 C 语言预处理器的行为。
因此,宏中不能使用像 if(ARGV1)、if(DEFINED ARGC)、foreach(loop_var IN LISTS ARGN) 这样的命令,因为这些标识符在宏中不是变量。
验证对比:
macro(abc arg1 arg2)
if(DEFINED ARGC)
message(true)
else()
message(false)
endif()
endmacro()
function(xyz arg1 arg2)
if(DEFINED ARGC)
message(true)
else()
message(false)
endif()
endfunction()
abc(A B C D) # 打印 false — ARGC 不是变量
xyz(A B C D) # 打印 true — ARGC 是变量
宏 abc 中 ARGC 不是变量,在执行宏之前会被替换为实际值(例如 4),因此 if(DEFINED ARGC) 会判定为 false。而函数中 ARGC 是真正的变量,因此判定为 true。
区别二:作用域
这里需要区分两个层面的"作用域":
-
变量作用域:函数有自己的变量作用域,函数内
set()的变量是局部变量,不会影响调用者。而宏没有独立的变量作用域,宏内设置的变量会直接渗透到调用者的作用域中(因为宏本质是字符串替换,代码展开在调用处)。 -
定义可见性:在这一层面上,函数和宏是一样的——它们都是全局可见的。与函数类似,父源码中定义的宏可以在子源码中调用,子源码中定义的宏也可以在父源码中调用。
# 演示宏的变量渗透
macro(set_xyz)
set(my_var "Hello from macro") # 这个变量会渗透到调用者作用域
endmacro()
function(set_abc)
set(my_var "Hello from function") # 这个变量是函数局部变量,不会影响调用者
endfunction()
set_xyz()
message("${my_var}") # 输出: Hello from macro(宏设置的变量仍然存在)
set_abc()
message("${my_var}") # 仍然输出: Hello from macro(函数内设置的变量未渗透出来)
2.3 如何选择
- 如果需要封装一段代码并希望它有自己的变量作用域(不污染调用者),应使用函数(function)。
- 如果需要一段代码与调用者共享作用域,或需要类似 C 语言宏的行为(字符串替换),可使用宏(macro)。
3. 文件操作
CMake 提供了功能强大的 file() 命令,可以对文件进行读写、删除、重命名、创建目录等一系列操作。
3.1 写文件:写入与追加内容
file(WRITE <filename> <content>...)
file(APPEND <filename> <content>...)
WRITE:将<content>写入<filename>。如果文件不存在则创建;如果文件已存在则覆盖。APPEND:将<content>追加到<filename>末尾。
文件路径可使用绝对路径或相对路径,相对路径解释为相对于当前源码路径。
file(WRITE wtest.txt "Hello World!") # 生成 wtest.txt 文件
file(APPEND wtest.txt " China") # 追加内容到文件末尾
3.2 写文件:由内容生成文件
file(GENERATE OUTPUT output-file
<INPUT input-file|CONTENT content>
[CONDITION expression])
output-file:输出文件名,可带路径。INPUT input-file:指定输入文件,以输入文件的内容生成输出文件。CONTENT content:直接指定内容生成输出文件。CONDITION expression:条件表达式,为真时生成文件,否则不生成。
注意:相对路径解释为相对于当前源码的 BINARY_DIR 路径(即 build 目录),而非当前源码路径。
# 由 wtest.txt 内容生成 out1.txt
file(GENERATE OUTPUT out1.txt INPUT "${PROJECT_SOURCE_DIR}/wtest.txt")
# 由指定内容生成 out2.txt
file(GENERATE OUTPUT out2.txt CONTENT "This is the out2.txt file")
# 由指定内容生成 out3.txt,带条件控制
file(GENERATE OUTPUT out3.txt CONTENT "This is the out3.txt file" CONDITION 1)
3.3 读文件:字节读取
file(READ <filename> <variable>
[OFFSET <offset>] [LIMIT <max-in>] [HEX])
从 <filename> 中读取内容并存储到 <variable> 中。可选的参数:
OFFSET <offset>:从指定偏移位置开始读取。LIMIT <max-in>:最多读取<max-in>字节。HEX:将数据转换为十六进制表示(对二进制数据有用)。
file(READ "${PROJECT_SOURCE_DIR}/wtest.txt" out_var) # 读取整个文件
message(${out_var})
file(READ "${PROJECT_SOURCE_DIR}/wtest.txt" out_var OFFSET 0 LIMIT 10) # 读取前 10 字节
message(${out_var})
file(READ "${PROJECT_SOURCE_DIR}/wtest.txt" out_var OFFSET 0 LIMIT 5 HEX) # 以十六进制读取前 5 字节
message(${out_var})
打印结果:
Hello World! China
Hello Worl
48656C6C6F
第一条语句读取了 wtest.txt 的完整内容(Hello World! China)。第二条语句从偏移 0 开始读取 10 字节,得到 Hello Worl。第三条语句同样从偏移 0 开始读取 5 字节,但以十六进制格式输出,Hello 对应的十六进制为 48656C6C6F。
3.4 以字符串形式读取文件
file(STRINGS <filename> <variable> [<options>...])
从文件中解析 ASCII 字符串列表并存储到 <variable> 中。该命令专用于读取字符串,会忽略二进制数据以及回车符(\r、CR)。
可选的 options 参数包括:
| 选项 | 说明 |
|---|---|
LENGTH_MAXIMUM <max-len> |
读取字符串的最大长度 |
LENGTH_MINIMUM <min-len> |
读取字符串的最小长度 |
LIMIT_COUNT <max-num> |
读取的行数 |
LIMIT_INPUT <max-in> |
读取的字节数 |
LIMIT_OUTPUT <max-out> |
存储到变量的限制字节数 |
NEWLINE_CONSUME |
把换行符也考虑进去 |
NO_HEX_CONVERSION |
禁止 Intel Hex 和 Motorola S-record 文件自动转换为二进制 |
REGEX <regex> |
只读取符合正则表达式的行 |
ENCODING <encoding-type> |
指定编码格式(UTF-8、UTF-16LE、UTF-16BE、UTF-32LE、UTF-32BE) |
# 从 input.txt 读取所有字符串
file(STRINGS "${PROJECT_SOURCE_DIR}/input.txt" out_var)
message("${out_var}")
# 限定字符串最大长度
file(STRINGS "${PROJECT_SOURCE_DIR}/input.txt" out_var LENGTH_MAXIMUM 5)
message("${out_var}")
# 限定字符串最小长度
file(STRINGS "${PROJECT_SOURCE_DIR}/input.txt" out_var LENGTH_MINIMUM 4)
message("${out_var}")
# 限定读取行数
file(STRINGS "${PROJECT_SOURCE_DIR}/input.txt" out_var LIMIT_COUNT 3)
message("${out_var}")
注:上述代码的打印结果取决于
input.txt文件的具体内容。简而言之,不加选项会读取所有行;LENGTH_MAXIMUM限制每行最大字符数;LENGTH_MINIMUM限制每行最小字符数;LIMIT_COUNT限制读取的行数。其余选项(如REGEX、ENCODING等)可按需组合使用。
3.5 计算文件的 Hash 值
file(<MD5|SHA1|SHA224|SHA256|SHA384|SHA512> <filename> <variable>)
计算指定文件内容的加密散列值(hash)并存储到变量中。必须指定一种哈希算法。
file(SHA256 "${PROJECT_SOURCE_DIR}/input.txt" out_var)
message("${out_var}")
打印结果示例(实际输出取决于文件内容):
2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824
3.6 文件重命名
file(RENAME <oldname> <newname>)
将 <oldname> 重命名为 <newname>。路径可使用绝对路径或相对路径,相对路径解释为相对于当前源码路径。
file(RENAME "${PROJECT_SOURCE_DIR}/input.txt" "${PROJECT_SOURCE_DIR}/output.txt")
3.7 删除文件
file(REMOVE [<files>...])
file(REMOVE_RECURSE [<files>...])
REMOVE:删除给定的文件,不能删除目录。REMOVE_RECURSE:删除给定的文件或目录(包括非空目录)。
file(REMOVE "${PROJECT_SOURCE_DIR}/out1.txt")
file(REMOVE_RECURSE "${PROJECT_SOURCE_DIR}/out2.txt"
"${PROJECT_SOURCE_DIR}/empty-dir"
"${PROJECT_SOURCE_DIR}/Non_empty-dir")
file()命令功能非常强大,除了上述基本功能外,还支持文件下载、文件锁等高级功能,有兴趣可以进一步了解。
4. 设置交叉编译
默认情况下,CMake 使用主机系统(运行 cmake 命令的操作系统)的编译器来编译工程,生成的可执行文件只能在主机系统上运行。如果希望编译得到的程序能在 ARM 开发板等目标平台上运行,则需要配置交叉编译。
4.1 交叉编译的基本配置
配置交叉编译的核心是设置以下几个 CMake 变量(配置代码必须放在 project() 命令之前,否则不会生效):
set(CMAKE_SYSTEM_NAME Linux) # 设置目标系统名称
set(CMAKE_SYSTEM_PROCESSOR arm) # 设置目标处理器架构
# 指定编译器的 sysroot 路径
set(TOOLCHAIN_DIR /opt/fsl-imx-x11/4.1.15-2.1.0/sysroots)
set(CMAKE_SYSROOT ${TOOLCHAIN_DIR}/cortexa7hf-neon-poky-linux-gnueabi)
# 指定交叉编译器
set(CMAKE_C_COMPILER ${TOOLCHAIN_DIR}/x86_64-pokysdk-linux/usr/bin/arm-poky-linux-gnueabi/arm-poky-linux-gnueabi-gcc)
set(CMAKE_CXX_COMPILER ${TOOLCHAIN_DIR}/x86_64-pokysdk-linux/usr/bin/arm-poky-linux-gnueabi/arm-poky-linux-gnueabi-g++)
# 为编译器添加编译选项
set(CMAKE_C_FLAGS "-march=armv7ve -mfpu=neon -mfloat-abi=hard -mcpu=cortex-a7")
set(CMAKE_CXX_FLAGS "-march=armv7ve -mfpu=neon -mfloat-abi=hard -mcpu=cortex-a7")
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
各变量说明:
CMAKE_SYSTEM_NAME:目标主机的操作系统名称,设置为Linux表示目标为 Linux 系统。CMAKE_SYSTEM_PROCESSOR:目标架构名称(如arm)。CMAKE_SYSROOT:该值会传递给编译器的--sysroot选项,指定编译器的系统根目录。编译过程中需要链接的库和头文件会去该目录下寻找(如标准 C 库和头文件)。CMAKE_C_COMPILER/CMAKE_CXX_COMPILER:指定交叉编译的 C/C++ 编译器路径。CMAKE_C_FLAGS/CMAKE_CXX_FLAGS:为编译器添加编译选项。CMAKE_FIND_ROOT_PATH_MODE_PROGRAM:控制find_program()是否搜索CMAKE_SYSROOT中的路径。设为NEVER则仅使用主机系统路径。CMAKE_FIND_ROOT_PATH_MODE_LIBRARY:控制find_library()是否搜索CMAKE_SYSROOT中的路径。设为ONLY则仅搜索CMAKE_SYSROOT中的路径。CMAKE_FIND_ROOT_PATH_MODE_INCLUDE:控制find_file()和find_path()是否搜索CMAKE_SYSROOT中的路径。设为ONLY则仅搜索CMAKE_SYSROOT中的路径。
注意:
CMAKE_SYSROOT、CMAKE_C_COMPILER、CMAKE_CXX_COMPILER等变量中涉及交叉编译工具的安装路径,需要根据实际安装路径进行设置。
4.2 CMake 版本问题
需要特别注意的是,低版本的 CMake 可能不支持上述交叉编译配置方式。例如 Ubuntu 系统自带的 CMake 3.5.1 版本可能会报错。建议下载使用高版本的 CMake(如 3.16.0)。
安装高版本 CMake 的简便方法:从 CMake 的 GitHub Release 页面(https://github.com/Kitware/CMake/releases ) 下载预编译的压缩包(如 cmake-3.16.0-Linux-x86_64.tar.gz),解压后即可直接使用其中的 cmake 可执行程序,无需自行编译。
4.3 使用工具链文件(推荐做法)
将交叉编译配置直接写入 CMakeLists.txt 虽然可行,但不够规范。更规范的做法是将所有交叉编译配置项单独写在一个配置文件中(工具链文件),然后通过 cmake 命令的 -DCMAKE_TOOLCHAIN_FILE 选项来指定该文件。
步骤:
-
在工程源码目录下创建工具链配置文件,如
arm-linux-setup.cmake,内容与上述配置代码相同。 -
从
CMakeLists.txt中移除这些交叉编译配置项,只保留工程基本配置:
cmake_minimum_required(VERSION 3.5)
project(HELLO)
add_executable(main main.c)
- 进入 build 目录,执行 cmake 时指定工具链文件:
cmake -DCMAKE_TOOLCHAIN_FILE=../arm-linux-setup.cmake ..
通过这种方式,CMakeLists.txt 更加清晰简洁,交叉编译配置独立管理,便于在不同平台间切换。
4.4 验证交叉编译结果
配置完成后,执行 make 编译,生成的二进制文件即为 ARM 架构的可执行程序(可通过 file 命令查看确认),可以将其拷贝到开发板上运行。
4.5 适配其他交叉编译器(以 arm-linux-gnueabihf-gcc 为例)
如果使用不同的交叉编译器,只需修改工具链文件中对应的变量即可。以常见的 arm-linux-gnueabihf-gcc 为例,对比原示例需要修改以下内容:
| 配置项 | 原示例(poky) | 改用 arm-linux-gnueabihf |
|---|---|---|
| C 编译器 | arm-poky-linux-gnueabi-gcc |
arm-linux-gnueabihf-gcc |
| C++ 编译器 | arm-poky-linux-gnueabi-g++ |
arm-linux-gnueabihf-g++ |
| sysroot 路径 | /opt/fsl-imx-x11/.../cortexa7hf-neon-poky-linux-gnueabi |
/usr/arm-linux-gnueabihf/(或工具链实际安装路径) |
| 浮点选项 | -mfloat-abi=hard |
gnueabihf 中的 hf 已隐含 hard-float,但仍可显式指定 |
对应的工具链文件 arm-linux-gnueabihf-setup.cmake 示例如下:
##################################
# 配置 ARM 交叉编译 (arm-linux-gnueabihf)
##################################
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR arm)
# 方法一(推荐):交叉编译器已添加到 PATH 环境变量中
# 此时只需指定编译器名称,无需完整路径
set(CMAKE_C_COMPILER arm-linux-gnueabihf-gcc)
set(CMAKE_CXX_COMPILER arm-linux-gnueabihf-g++)
# apt 安装的 sysroot 通常在此路径,用以下命令确认:
# arm-linux-gnueabihf-gcc --print-sysroot
set(CMAKE_SYSROOT /usr/arm-linux-gnueabihf)
# ============================================================
# 方法二(自定义路径,如 /opt/toolchains):
# 使用方法二时,必须把上面方法一的 3 行 set() 全部注释掉,
# 否则方法二的设置会被方法一覆盖,不会生效。
# ============================================================
# set(TOOLCHAIN_DIR /opt/toolchains/gcc-arm-9.2-2019.12-x86_64-arm-linux-gnueabihf)
# set(CMAKE_SYSROOT ${TOOLCHAIN_DIR}/arm-linux-gnueabihf/libc)
# set(CMAKE_C_COMPILER ${TOOLCHAIN_DIR}/bin/arm-linux-gnueabihf-gcc)
# set(CMAKE_CXX_COMPILER ${TOOLCHAIN_DIR}/bin/arm-linux-gnueabihf-g++)
# 为编译器添加编译选项(根据目标 ARM 架构调整)
set(CMAKE_C_FLAGS "-march=armv7-a -mfpu=neon -mfloat-abi=hard")
set(CMAKE_CXX_FLAGS "-march=armv7-a -mfpu=neon -mfloat-abi=hard")
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
##################################
# end
##################################
关键变化说明:
- 编译器名称:直接将
arm-poky-linux-gnueabi-*替换为arm-linux-gnueabihf-*。如果编译器已加入PATH环境变量,只需写名称即可;否则需写完整路径。 - sysroot 路径:
arm-linux-gnueabihf工具链的 sysroot 通常是/usr/arm-linux-gnueabihf/(apt 安装时)或工具链目录下的arm-linux-gnueabihf/libc(自定义安装时)。务必通过arm-linux-gnueabihf-gcc --print-sysroot命令确认实际路径。 - 编译选项:
-mfloat-abi=hard虽已由gnueabihf隐含,但显式指定更安全。-march和-mfpu需根据具体目标芯片调整(例如 Cortex-A7 可使用-mcpu=cortex-a7替代-march=armv7-a)。 - 其余变量(
CMAKE_SYSTEM_NAME、FIND_ROOT_PATH_MODE_*等)保持不变。
通用规则:无论使用哪种交叉编译器,核心都是替换
CMAKE_C_COMPILER、CMAKE_CXX_COMPILER和CMAKE_SYSROOT这三个变量,其余配置保持通用即可。
5. 变量的作用域
与 C 语言类似,CMake 中的变量也有作用域的概念。本节从三个方面来介绍:函数作用域、目录作用域和全局作用域。
5.1 函数作用域(Function Scope)
当在函数内通过 set() 将变量与当前函数作用域绑定时,该变量仅在函数作用域内有效。出了这个作用域,如果外部也有同名变量,则使用外部的同名变量。若函数内嵌套调用其他函数,内层函数会依次向外搜索变量。
5.1.1 函数内引用外部定义的变量
function(xyz)
message(${ABC}) # 引用外部变量 ABC
endfunction()
set(ABC "Hello World") # 定义变量 ABC
xyz() # 调用函数
打印结果:Hello World
由此可见,函数内部可以引用函数外部定义的变量。
5.1.2 函数内定义的变量外部不可引用
function(xyz)
set(ABC "Hello World") # 函数内定义变量 ABC
endfunction()
xyz()
if(DEFINED ABC)
message("true")
message("${ABC}")
else()
message("false")
endif()
打印结果:false
这说明函数内部定义的变量仅在函数内部可用,出了函数便无效,类似于 C 语言中的局部变量。
5.1.3 函数内定义与外部同名的变量
function(xyz)
message("函数内部")
message("${ABC}")
set(ABC "Hello China!") # 设置变量 ABC
message("${ABC}")
endfunction()
set(ABC "Hello World!") # 外部定义变量 ABC
xyz()
message("函数外部")
message("${ABC}")
打印结果:
函数内部
Hello World!
Hello China!
函数外部
Hello World!
从打印信息可知,函数内部调用 set() 并不是修改外部变量,而是新建了一个同名的局部变量。在调用 set() 之前,函数内部没有定义 ABC,所以会向外搜索到外部定义的 Hello World!;调用 set() 之后,函数内创建了自己的 ABC,此时引用的是局部变量 Hello China!。函数外部打印的 ABC 仍然是原来的 Hello World!,并未被修改。
5.1.4 使用 PARENT_SCOPE 修改外部变量
如果需要在函数内修改外部定义的变量,可以在 set() 命令末尾加上 PARENT_SCOPE 关键字:
function(xyz)
set(ABC "Hello China!" PARENT_SCOPE) # 加上 PARENT_SCOPE
endfunction()
set(ABC "Hello World!")
xyz()
message("${ABC}")
打印结果:Hello China!
PARENT_SCOPE 的含义:如果添加了 PARENT_SCOPE 选项,则变量将设置在当前作用域的上一层作用域(父作用域)中,而不是在当前作用域内。每个目录(包含 CMakeLists.txt 的目录)或函数都会创建一个新作用域。
示例 1:函数的上一层作用域是调用者所在的作用域
function(xyz)
set(ABC "Hello China!" PARENT_SCOPE)
endfunction()
set(ABC "Hello World!")
xyz()
message("${ABC}") # Hello China! — 修改了调用者作用域中的 ABC
函数 xyz 的上一层作用域就是调用 xyz() 时所在的作用域(当前目录作用域)。
示例 2:嵌套函数调用
function(func2)
set(ABC "Hello People!" PARENT_SCOPE)
endfunction()
function(func1)
set(ABC "Hello China!")
func2()
endfunction()
set(ABC "Hello World!")
func1()
message("${ABC}")
打印结果:Hello People!
函数 func2 的上一层作用域是 func1 函数的作用域,所以 func2 中通过 PARENT_SCOPE 修改的是 func1 中的 ABC(将 Hello China! 改为 Hello People!)。但 func1 中的 ABC 是局部变量,不影响外部,所以最终外部打印的仍是 Hello People!(因为 func2 修改了 func1 的局部变量)。
示例 3:跨目录的 PARENT_SCOPE
假设有如下目录结构:
├── CMakeLists.txt
└── src
└── CMakeLists.txt
顶层 CMakeLists.txt:
cmake_minimum_required(VERSION 3.5)
project(TEST)
add_subdirectory(src)
message("${ABC}")
src/CMakeLists.txt:
set(ABC "Hello World!" PARENT_SCOPE)
这种情况下,PARENT_SCOPE 的上一层作用域是顶层目录作用域,因此变量 ABC 会在顶层源码中被设置。
5.1.5 函数返回值的实现
前面提到过,return() 不能用于返回参数。函数的返回值实际上是通过 PARENT_SCOPE 来实现的。
cmake_minimum_required(VERSION 3.5)
project(TEST)
# 定义一个函数,实现两个数相加,并将结果通过 out 参数返回给调用者
function(xyz out var1 var2)
math(EXPR temp "${var1} + ${var2}")
set(${out} ${temp} PARENT_SCOPE)
endfunction()
xyz(out_var 5 10)
message("${out_var}")
打印结果:15
原理拆解:
- 调用
xyz(out_var 5 10)时,形参out接收到的值是字符串"out_var",形参var1为5、var2为10。 - 函数内
set(${out} ${temp} PARENT_SCOPE)中的${out}展开为"out_var",等价于执行set(out_var 15 PARENT_SCOPE)。 - 所以实际上是以传入的参数值(
out_var)作为变量名,在父作用域中创建了一个变量。调用结束后,外部便多了一个名为out_var、值为15的变量。
如果调用时传入的是其他名字,例如
xyz(result 5 10),那么外部创建的变量名就是result。这就是实现"参数返回"的机制——函数通过${out}间接引用调用者想要的变量名。
5.2 目录作用域(Directory Scope)
子目录会将父目录的所有变量拷贝一份到当前 CMakeLists.txt 中,当前 CMakeLists.txt 中变量的作用域仅在当前目录有效。
目录作用域有两个特点:
- 向下有效:上层作用域中定义的变量在下层作用域中有效。
- 值拷贝:子目录从父目录中拷贝一份变量的副本,子目录修改变量不影响父目录中的原变量。
示例:目录结构如下
├── CMakeLists.txt
└── sub_dir
└── CMakeLists.txt
父目录 CMakeLists.txt:
cmake_minimum_required(VERSION 3.5)
project(TEST)
set(parent_var "Hello parent")
message("parent-<parent_var>: ${parent_var}")
add_subdirectory(sub_dir)
message("parent-<parent_var>: ${parent_var}")
子目录 sub_dir/CMakeLists.txt:
message("subdir-<parent_var>: ${parent_var}")
set(parent_var "Hello child")
message("变量修改之后")
message("subdir-<parent_var>: ${parent_var}")
打印结果:
parent-<parent_var>: Hello parent
subdir-<parent_var>: Hello parent
变量修改之后
subdir-<parent_var>: Hello child
parent-<parent_var>: Hello parent
分析:
- 父目录定义了
parent_var = "Hello parent"。 add_subdirectory(sub_dir)加载子目录时,子目录会拷贝父目录的所有变量,因此子目录中第一次打印parent_var为Hello parent。- 子目录中执行
set(parent_var "Hello child")修改的是子目录自己的副本,不影响父目录中的原变量。 - 回到父目录后,
parent_var仍然是原来的Hello parent。
5.3 全局作用域(缓存变量 / Cache Variables)
缓存变量(Cache Variables)在整个 CMake 工程的编译生命周期内都有效,作用域是全局范围的,工程内其他任意目录都可以访问。
定义缓存变量有多种方式:
方式一:使用 set() 命令添加 CACHE 选项
set(VAR_NAME value CACHE STRING "Description")
方式二:使用 cmake -D 选项
cmake -DVAR_NAME=value ..
-D 选项会创建一个名为 VAR_NAME 的缓存变量(即全局变量),在整个工程中生效。
6. 属性(Properties)
CMake 中的属性会影响一些行为,大致可分为全局属性、目录属性、目标属性等。官方文档:cmake-properties
6.1 目录属性(Directory Properties)
目录属性即 CMakeLists.txt 源码的属性。常用的目录属性包括:
| 属性名 | 说明 |
|---|---|
CACHE_VARIABLES |
当前目录中可用的缓存变量列表 |
CLEAN_NO_CUSTOM |
若为 true,make clean 时不删除自定义命令的输出文件 |
INCLUDE_DIRECTORIES |
头文件搜索路径列表(include_directories() 命令所添加的目录) |
LINK_DIRECTORIES |
库文件搜索路径列表(link_directories() 命令所添加的目录) |
MACROS |
当前目录中可用的宏命令列表 |
PARENT_DIRECTORY |
当前子目录的父源码路径(顶级目录时为空字符串,只读) |
VARIABLES |
当前目录中定义的变量列表(只读) |
6.1.1 获取和设置目录属性
获取目录属性使用 get_directory_property() 命令:
get_directory_property(<variable> [DIRECTORY <dir>] <prop-name>)
- 将属性的值存储在
variable变量中 DIRECTORY <dir>为可选参数,指定目录(默认是当前源码目录)prop-name为属性名称
设置目录属性使用 set_directory_properties() 命令:
set_directory_properties(PROPERTIES prop1 value1 prop2 value2)
6.1.2 INCLUDE_DIRECTORIES 属性详解
INCLUDE_DIRECTORIES 是头文件搜索路径列表。include_directories() 命令会将目录添加到此属性中。
cmake_minimum_required(VERSION 3.5)
project(TEST)
# 获取 INCLUDE_DIRECTORIES 属性(此时应为空)
get_directory_property(out_var INCLUDE_DIRECTORIES)
message("${out_var}")
# 添加头文件搜索目录
include_directories(include)
# 再次获取
get_directory_property(out_var INCLUDE_DIRECTORIES)
message("${out_var}")
# 使用 BEFORE 将目录添加到列表前面
include_directories(BEFORE hello)
# 再次获取
get_directory_property(out_var INCLUDE_DIRECTORIES)
message("${out_var}")
include_directories() 默认将目录添加到列表末尾,可使用 BEFORE 或 AFTER 控制添加到前面或后面。
直接设置 INCLUDE_DIRECTORIES 属性替代 include_directories()
除了使用 include_directories() 命令,也可以直接设置 INCLUDE_DIRECTORIES 属性来添加头文件搜索路径:
cmake_minimum_required(VERSION 3.5)
project(TEST)
# 直接设置 INCLUDE_DIRECTORIES 属性(必须使用绝对路径)
set_directory_properties(PROPERTIES INCLUDE_DIRECTORIES /home/dt/vscode_ws/cmake_test/include)
get_directory_property(out_var INCLUDE_DIRECTORIES)
message("${out_var}")
add_executable(main main.c)
注意:使用
set_directory_properties()设置属性时,路径需要使用绝对路径。
父目录属性向下传递
父目录的 INCLUDE_DIRECTORIES 属性可以初始化、填充子目录的 INCLUDE_DIRECTORIES 属性:
# 父源码
cmake_minimum_required(VERSION 3.5)
project(TEST)
include_directories(include hello)
get_directory_property(p_list INCLUDE_DIRECTORIES)
message("${p_list}")
add_subdirectory(subdir)
# 子源码
get_directory_property(c_list INCLUDE_DIRECTORIES)
message("${c_list}")
子目录中的 INCLUDE_DIRECTORIES 会继承父目录的设置。
6.1.3 LINK_DIRECTORIES 属性详解
LINK_DIRECTORIES 是库文件搜索路径列表。link_directories() 命令会将目录添加到此属性中。
cmake_minimum_required(VERSION 3.5)
project(TEST)
# 获取 LINK_DIRECTORIES 属性(此时应为空)
get_directory_property(out_var LINK_DIRECTORIES)
message("${out_var}")
# 添加库文件搜索目录
link_directories(include hello)
get_directory_property(out_var LINK_DIRECTORIES)
message("${out_var}")
同样,父目录的 LINK_DIRECTORIES 属性也会传递给子目录,也可以直接设置该属性来替代 link_directories() 命令。
6.2 目标属性(Target Properties)
目标属性是与目标(可执行文件、库等)关联的属性。可通过 get_target_property() 和 set_target_property() 命令获取或设置。
常用的目标属性包括:
| 属性名 | 说明 |
|---|---|
BINARY_DIR |
定义目标的目录中 CMAKE_CURRENT_BINARY_DIR 的值(只读) |
SOURCE_DIR |
定义目标的目录中 CMAKE_CURRENT_SOURCE_DIR 的值(只读) |
INCLUDE_DIRECTORIES |
目标的头文件搜索路径列表,target_include_directories() 会向此列表添加目录,初始值拷贝自目录属性的 INCLUDE_DIRECTORIES |
INTERFACE_INCLUDE_DIRECTORIES |
target_include_directories() 使用 PUBLIC 和 INTERFACE 关键字时填充此属性 |
INTERFACE_LINK_LIBRARIES |
target_link_libraries() 使用 PUBLIC 和 INTERFACE 关键字时填充此属性 |
LIBRARY_OUTPUT_DIRECTORY |
库文件的输出目录 |
LIBRARY_OUTPUT_NAME |
库文件的输出名称 |
LINK_LIBRARIES |
目标的链接依赖库列表 |
OUTPUT_NAME |
目标文件的输出名称 |
TYPE |
目标类型,取值为 STATIC_LIBRARY、MODULE_LIBRARY、SHARED_LIBRARY、INTERFACE_LIBRARY、EXECUTABLE 之一或内部目标类型 |

浙公网安备 33010602011771号