第一部分 基础知识
1、安装
cmake --version
:查看cmake版本,如果未安装会提示安装所需要的命令。
2、使用
CMake在使用时通过编写CMakeLists.txt,然后执行cmake命令来生成Makefile文件。
CMake支持大写、小写、混合大小写的命令。如果在编写CMakeLists.txt文件时使用的工具有对应的命令提示,那么大小写随缘即可,不要太过在意。
3、编写
3.1 注释
1
2
3
4
5
# 这是个行注释
#[[这是一个块注释
这是一个块注释
这是一个块注释
]]
3.2 只有源文件
3.2.1 准备工作
add.c
1
2
3
4
5
6
7
#include <stdio.h>
#include "head.h"
int add(int a, int b)
{
return a+b;
}
div.c
1
2
3
4
5
6
7
#include <stdio.h>
#include "head.h"
double divide(int a, int b)
{
return (double)a/b;
}
mult.c
1
2
3
4
5
6
7
#include <stdio.h>
#include "head.h"
int multiply(int a, int b)
{
return a*b;
}
sub.c
1
2
3
4
5
6
7
#include <stdio.h>
#include "head.h"
int subtract(int a, int b)
{
return a-b;
}
main.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
#include "head.h"
int main()
{
int a = 20;
int b = 12;
printf("a = %d, b = %d\n", a, b);
printf("a + b = %d\n", add(a, b));
printf("a - b = %d\n", subtract(a, b));
printf("a * b = %d\n", multiply(a, b));
printf("a / b = %f\n", divide(a, b));
return 0;
}
head.h
1
2
3
4
5
6
7
8
9
10
11
#ifndef _HEAD_H
#define _HEAD_H
// 加法
int add(int a, int b);
// 减法
int subtract(int a, int b);
// 乘法
int multiply(int a, int b);
// 除法
double divide(int a, int b);
#endif
3.2.2 共处一室
上述文件的目录结构如下:
1
2
3
4
5
6
7
8
9
$ tree
.
├── add.c
├── CMakeLists.txt
├── div.c
├── head.h
├── main.c
├── mult.c
└── sub.c
1、编写CMakeLists.txt文件
1
2
3
cmake_minimum_required(VERSION 3.0)
project(CALC)
add_executable(app add.c div.c main.c mult.c sub.c)
-
cmake_minimum_required:指定使用的cmake的最低版本问,可选,非必须,但是不加会警告
-
project:定义工程名称,并可指定工程的版本、工程描述、web主页地址、支持的语言(默认情况支持所有语言),如果不需要这些都是可以忽略的,只需要指定工程名称即可。
1 2 3 4 5 6 7
# PROJECT 指令的语法是: project(<PROJECT-NAME> [<language-name>...]) project(<PROJECT-NAME> #项目名称 [VERSION <major>[.<minor>[.<patch>[.<tweak>]]]] #项目的版本 [DESCRIPTION <project-description-string>] #项目描述 [HOMEPAGE_URL <url-string>] #可随意指定URL [LANGUAGES <language-name>...]) #项目所使用的语言
-
add_executable:定义工程会生成一个可执行程序(可执行程序名与项目名称无关,可随意指定)
1 2 3 4 5
add_executable(可执行程序名 源文件名称) #如果有多个源文件则把所有源文件列举出来 #样式一:使用空格隔开每个源文件 add_executable(app add.c div.c main.c mult.c sub.c) #样式二:使用分号隔开每个源文件 add_executable(app add.c;div.c;main.c;mult.c;sub.c)
2、执行CMake命令
使用CMake构建项目需要两步如下:
1
2
cmake . #(.代表CMakeLists.txt所在的路径是当前路径)
make
执行完cmake命令后,当前文件夹就会多出许多文件,其中就有makefile
文件,此时再执行make
命令就可以对项目进行构建。
3.2.3 VIP包房
cmake
命令会在其执行时所在的路径下创建一些目录和文件造成目录很混乱,所以我们可以创建一个新文件夹,在新文件夹中执行cmake
命令。
1
2
3
4
mkdir build #文件夹名字任意起
cd build #在build文件夹中执行cmake
cmake .. #(CMakeLists.txt在该文件夹的上一级,所以使用..)
make #make该文件夹下的Makefile
这样当命令执行完毕之后,只会在build目录中会生成目录和文件。
3.3 私人定制
3.3.1 定义变量
在上面的例子中一共提供了5个源文件,假设这五个源文件需要反复被使用,每次都直接将它们的名字写出来确实是很麻烦,此时我们就需要定义一个变量,将文件名对应的字符串存储起来,在cmake里定义变量需要使用set
。
1
2
3
4
5
6
7
8
9
10
11
# SET 指令的语法是:
# [] 中的参数为可选项, 如不需要可以不写
SET(VAR [VALUE] [CACHE TYPE DOCSTRING [FORCE]])
# 使用方式:
# 方式1: 各个源文件之间使用空格间隔
# set(SRC_LIST add.c div.c main.c mult.c sub.c)
# 方式2: 各个源文件之间使用分号 ; 间隔
set(SRC_LIST add.c;div.c;main.c;mult.c;sub.c)
add_executable(app ${SRC_LIST})
3.3.2 指定使用的C++的标准
在编写C++程序的时候,可能会用到C++11、C++14、C++17、C++20等新特性,那么就需要在编译的时候在编译命令中制定出要使用哪个标准:
1
g++ *.cpp -std=c++11 -o app
上面的例子中通过参数-std=c++11
指定出要使用c++11标准编译程序,C++标准对应有一个宏叫做DCMAKE_CXX_STANDARD
。在CMake中想要指定C++标准有两种方式:
- 在 CMakeLists.txt 中通过 set 命令指定
1
2
3
4
5
6
#增加-std=c++11
set(CMAKE_CXX_STANDARD 11)
#增加-std=c++14
set(CMAKE_CXX_STANDARD 14)
#增加-std=c++17
set(CMAKE_CXX_STANDARD 17)
- 在执行 cmake 命令的时候指定出这个宏的值
1
2
3
4
5
6
#增加-std=c++11
cmake CMakeLists.txt文件路径 -DCMAKE_CXX_STANDARD=11
#增加-std=c++14
cmake CMakeLists.txt文件路径 -DCMAKE_CXX_STANDARD=14
#增加-std=c++17
cmake CMakeLists.txt文件路径 -DCMAKE_CXX_STANDARD=17
3.3.3 指定输出的路径
在CMake中指定可执行程序输出的路径,也对应一个宏,叫做EXECUTABLE_OUTPUT_PATH
,它的值还是通过set命令进行设置:
1
2
set(HOME /home/robin/Linux/Sort) #存储一个绝对路径
set(EXECUTABLE_OUTPUT_PATH ${HOME}/bin) #将拼接好的路径值赋给EXECUTABLE_OUTPUT_PATH宏(如果路径的子目录不存在则自动创建)
由于可执行程序是基于 cmake 命令生成的 makefile 文件然后再执行 make 命令得到的,所以如果此处指定可执行程序生成路径的时候使用的是相对路径 ./xxx/xxx,那么这个路径中的 ./ 对应的就是 makefile 文件所在的那个目录(建议使用绝对路径)。
示例
1
2
3
4
5
6
cmake_minimum_required(VERSION 3.0)
project(CALC)
set(SRC add.c div.c main.c mult.c sub.c)
set(EXECUTABLE_OUTPUT_PATH ..)
set(CMAKE_CXX_STANDARD 11)
add_executable(app ${SRC})
3.4 搜索文件
如果一个项目里边的源文件很多,在编写CMakeLists.txt文件的时候不可能将项目目录的各个文件一一罗列出来,这样太麻烦也不现实。所以,在CMake中为我们提供了搜索文件的命令,可以使用aux_source_directory
命令或者file
命令。
3.4.1 方式一:aux_source_directory命令
在 CMake 中使用aux_source_directory
命令可以查找某个路径下的所有源文件,命令格式为:
1
aux_source_directory(< dir > < variable >)
- dir:要搜索的目录
- variable:将从dir目录下搜索到的源文件列表存储到该变量中
例子
1
2
3
4
5
6
7
8
cmake_minimum_required(VERSION 3.0)
project(CALC)
# 搜索 src 目录下的源文件,并且支持同时搜索多个目录
#CMAKE_CURRENT_SOURCE_DIR表示当前处理的 CMakeLists.txt 文件所在的目录的路径
aux_source_directory(${CMAKE_CURRENT_SOURCE_DIR}/srca SRC_LIST_A)
aux_source_directory(${CMAKE_CURRENT_SOURCE_DIR}/srcb SRC_LIST_B)
set(SRC_LIST "${SRC_LIST_A} ${SRC_LIST_B}")
add_executable(app ${SRC_LIST})
3.4.2 方式二:file命令
file
命令也可以用来搜索文件,除了搜索以外通过 file 还可以做其他事情。
1
file(GLOB/GLOB_RECURSE 变量名 要搜索的文件路径和文件类型)
-
GLOB: 将指定目录下搜索到的满足条件的所有文件名生成一个列表,并将其存储到变量中。
-
GLOB_RECURSE:递归搜索指定目录,将搜索到的满足条件的文件名生成一个列表,并将其存储到变量中。
例子
1
2
3
4
5
6
file(GLOB_RECURSE MAIN_SRC ${CMAKE_CURRENT_SOURCE_DIR}/src/*.c) #CMAKE_CURRENT_SOURCE_DIR表示当前处理的 CMakeLists.txt 文件所在的目录的路径
file(GLOB MAIN_HEAD ${CMAKE_CURRENT_SOURCE_DIR}/include/*.h)
#路径名可以加双引号,也可以不加
file(GLOB_RECURSE MAIN_SRC "${CMAKE_CURRENT_SOURCE_DIR}/src/*.c")
file(GLOB MAIN_HEAD "${CMAKE_CURRENT_SOURCE_DIR}/include/*.h")
3.5 包含头文件
在编译项目源文件的时候,很多时候都需要将源文件对应的头文件路径指定出来,这样才能保证在编译过程中编译器能够找到这些头文件,并顺利通过编译。在CMake中设置要包含的目录也很简单,通过一个命令就可以搞定了,他就是include_directories
。
1
include_directories(headfilepath)
假设目录结构如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ tree
.
├── build
├── CMakeLists.txt
├── include
│ └── head.h
└── src
├── add.c
├── div.c
├── main.c
├── mult.c
└── sub.c
3 directories, 7 files
可以使用以下命令指定编译过程中需要搜索的头文件目录
1
2
3
4
5
6
7
8
9
cmake_minimum_required(VERSION 3.0)
project(CALC)
set(CMAKE_CXX_STANDARD 11)
set(HOME /home/Linux/calc) #路径可以随便设置
set(EXECUTABLE_OUTPUT_PATH ${HOME}/bin/)
#PROJECT_SOURCE_DIR表示最近一次调用 project() 命令的目录的路径,也就是在执行cmake命令时后边带的路径
include_directories(${PROJECT_SOURCE_DIR}/include)
file(GLOB SRC_LIST ${CMAKE_CURRENT_SOURCE_DIR}/src/*.c)
add_executable(app ${SRC_LIST})
3.6 制作动态库或静态库
有些时候我们编写的源代码并不需要将他们编译生成可执行程序,而是生成一些静态库或动态库提供给第三方使用,下面来讲解在cmake中生成这两类库文件的方法。
1
2
3
4
5
6
7
8
9
10
11
.
├── build
├── CMakeLists.txt
├── include # 头文件目录
│ └── head.h
├── main.c # 制作库文件不需要main.c
└── src # 源文件目录
├── add.c
├── div.c
├── mult.c
└── sub.c
3.6.1 制作静态库
在cmake中,如果要制作静态库,需要使用的命令如下:
1
add_library(库名称 STATIC 源文件1 [源文件2] ...)
在Linux中,静态库名字分为三部分:lib+库名字+.a,此处只需要指定出库的名字就可以了,另外两部分在生成该文件的时候会自动填充。
在Windows中虽然库名和Linux格式不同,但也只需指定出名字即可。
举例:
1
2
3
4
5
cmake_minimum_required(VERSION 3.0)
project(CALC)
include_directories(${PROJECT_SOURCE_DIR}/include)
file(GLOB SRC_LIST "${CMAKE_CURRENT_SOURCE_DIR}/src/*.c")
add_library(calc STATIC ${SRC_LIST})
3.6.2 制作动态库
在cmake中,如果要制作动态库,需要使用的命令如下:
1
add_library(库名称 SHARED 源文件1 [源文件2] ...)
在Linux中,动态库名字分为三部分:lib+库名字+.so,此处只需要指定出库的名字就可以了,另外两部分在生成该文件的时候会自动填充。
在Windows中虽然库名和Linux格式不同,但也只需指定出名字即可。
举例:
1
2
3
4
5
cmake_minimum_required(VERSION 3.0)
project(CALC)
include_directories(${PROJECT_SOURCE_DIR}/include)
file(GLOB SRC_LIST "${CMAKE_CURRENT_SOURCE_DIR}/src/*.c")
add_library(calc SHARED ${SRC_LIST})
3.6.3 指定输出的路径
- 方式一
由于在Linux下生成的动态库默认是有执行权限的,所以可以按照生成可执行程序的方式去指定它生成的目录:
1
2
3
4
5
6
7
cmake_minimum_required(VERSION 3.0)
project(CALC)
include_directories(${PROJECT_SOURCE_DIR}/include)
file(GLOB SRC_LIST "${CMAKE_CURRENT_SOURCE_DIR}/src/*.c")
# 设置动态库生成路径
set(EXECUTABLE_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/lib)
add_library(calc SHARED ${SRC_LIST})
注:这种方法不适用于静态库。
- 方式二
由于在Linux下生成的静态库默认不具有可执行权限,所以在指定静态库生成的路径的时候就不能使用EXECUTABLE_OUTPUT_PATH宏了,而应该使用LIBRARY_OUTPUT_PATH,这个宏对应静态库文件和动态库文件都适用。
1
2
3
4
5
6
7
8
9
10
cmake_minimum_required(VERSION 3.0)
project(CALC)
include_directories(${PROJECT_SOURCE_DIR}/include)
file(GLOB SRC_LIST "${CMAKE_CURRENT_SOURCE_DIR}/src/*.c")
# 设置动态库/静态库生成路径
set(LIBRARY_OUTPUT_PATH ${PROJECT_SOURCE_DIR}/lib)
# 生成动态库
#add_library(calc SHARED ${SRC_LIST})
# 生成静态库
add_library(calc STATIC ${SRC_LIST})
3.7 包含库文件
在编写程序的过程中,可能会用到一些系统提供的动态库或者自己制作出的动态库或者静态库文件,cmake中也为我们提供了相关的加载动态库的命令。
在cmake
中链接静态库与动态库的命令如下:
1
2
3
4
target_link_libraries(
<target>
<PRIVATE|PUBLIC|INTERFACE> <item>...
[<PRIVATE|PUBLIC|INTERFACE> <item>...]...)
- target: 指定要加载动态库的文件名(不是动态库文件)
- 该文件可以是源文件
- 该文件可以是动态库或静态库文件
- 该文件可以是可执行文件
-
**PRIVATE PUBLIC INTERFACE**:动态库的访问权限,默认为 PUBLIC
- 如果各个动态库之间没有依赖关系,无需做任何设置,三者没有没有区别,一般无需指定,使用默认的 PUBLIC 即可。
动态库的链接具有传递性
,如果动态库 A 链接了动态库B、C,动态库D链接了动态库A,此时动态库D相当于也链接了动态库B、C,并可以使用动态库B、C中定义的方法。PUBLIC
:在public后面的库会被Link到前面的target中,并且里面的符号也会被导出,提供给第三方使用。PRIVATE
:在private后面的库仅被link到前面的target中,并且终结掉,第三方不能感知你调了啥库。INTERFACE
:在interface后面引入的库不会被链接到前面的target中,只会导出符号。
- 静态库的链接不具有传递性。
3.7.1 链接静态库
1
2
3
4
5
6
7
8
9
10
11
12
13
$ tree
.
├── build
├── CMakeLists.txt
├── include
│ └── head.h
├── lib
│ └── libcalc.a # 制作出的静态库的名字
│ └── libcalc.so # 制作出的动态库的名字
└── src
└── main.c
4 directories, 4 files
举例:
静态库链接有两种方法
方法一:使用全局变量
1
2
3
4
5
6
7
8
9
10
11
12
# 使用全局变量
cmake_minimum_required(VERSION 3.0)
project(CALC)
# 搜索指定目录下源文件
file(GLOB SRC_LIST ${CMAKE_CURRENT_SOURCE_DIR}/src/*.c)
# 在使用静态库时需要包含头文件路径
include_directories(${PROJECT_SOURCE_DIR}/include)
# 包含静态库路径
link_directories(${PROJECT_SOURCE_DIR}/lib)
# 链接静态库
link_libraries(calc)
add_executable(app ${SRC_LIST})
上面的例子使用了全局变量,在现代CMake不推荐
方法二:现代化方法
1
2
3
4
5
6
7
8
9
10
11
cmake_minimum_required(VERSION 3.0)
project(CALC)
# 搜索指定目录下的源文件
file(GLOB SRC_LIST ${CMAKE_CURRENT_SOURCE_DIR}/src/*.c)
# 添加可执行文件
add_executable(app ${SRC_LIST})
# 在使用静态库时需要包含头文件路径
target_include_directories(app PRIVATE ${PROJECT_SOURCE_DIR}/include)
# 链接静态库
target_link_directories(app PRIVATE ${PROJECT_SOURCE_DIR}/lib)
target_link_libraries(app PRIVATE calc) #calc是库文件名称
注:在使用静态库需要包含头文件路径.
3.7.2 链接动态库
动态库的链接和静态库在计算机内存中是不同的:
- 静态库会在生成可执行程序的链接阶段被打包到可执行程序中,所以可执行程序启动,静态库就被加载到内存中了。
- 动态库在生成可执行程序的链接阶段不会被打包到可执行程序中,当可执行程序被启动并且调用了动态库中的函数的时候,动态库才会被加载到内存
但是在CMake中可以使用相同的方法
1
2
3
4
5
6
7
8
9
10
11
12
cmake_minimum_required(VERSION 3.0)
project(CALC)
# 搜索指定目录下的源文件
file(GLOB SRC_LIST ${CMAKE_CURRENT_SOURCE_DIR}/src/*.c)
# 添加可执行文件目标
add_executable(app ${SRC_LIST})
# 在使用动态库时需要包含头文件路径
target_include_directories(app PRIVATE ${PROJECT_SOURCE_DIR}/include)
# 包含动态库路径
target_link_directories(app PRIVATE ${PROJECT_SOURCE_DIR}/lib)
# 链接动态库
target_link_libraries(app PRIVATE calc)
3.7.3 区别
链接动态库和静态库在 CMake 中使用的基本命令是相同的,但它们的行为和处理方式有所不同。链接时,CMake 负责解析库之间的依赖关系,并将必要的库包含在链接过程中。
动态库和静态库的区别
- 动态库(Shared Library):在运行时加载,链接器只需要知道动态库的名称及其路径。动态库具有传递性,如果一个动态库依赖于另一个动态库,链接器会自动处理这些依赖关系。
- 静态库(Static Library):在链接时将代码打包到最终的可执行文件中。静态库没有传递性依赖,你需要显式地链接所有依赖的静态库。
3. 8 日志
在CMake中可以打印消息,该命令的名字为message
:
1
message([STATUS|WARNING|AUTHOR_WARNING|FATAL_ERROR|SEND_ERROR] "message to display" ...)
(无)
:重要消息STATUS
:非重要消息WARNING
:CMake 警告, 会继续执行AUTHOR_WARNING
:CMake 警告 (dev), 会继续执行SEND_ERROR
:CMake 错误, 继续执行,但是会跳过生成的步骤FATAL_ERROR
:CMake 错误, 终止所有处理过程
CMake的命令行工具会在stdout(标准输出)上显示STATUS
消息,在stderr上显示其他所有消息。CMake的GUI会在它的log区域显示所有消息。
CMake警告和错误消息的文本显示使用的是一种简单的标记语言。文本没有缩进,超过长度的行会回卷,段落之间以新行做为分隔符。
举例:
1
2
3
4
5
6
# 输出一般日志信息
message(STATUS "source path: ${PROJECT_SOURCE_DIR}")
# 输出警告信息
message(WARNING "source path: ${PROJECT_SOURCE_DIR}")
# 输出错误信息
message(FATAL_ERROR "source path: ${PROJECT_SOURCE_DIR}")
3.9 变量操作
3.9.1 拼接
有时候项目中的源文件并不一定都在同一个目录中,但是这些源文件最终却需要一起进行编译来生成最终的可执行文件或者库文件。如果我们通过file
命令对各个目录下的源文件进行搜索,最后还需要做一个字符串拼接的操作,关于字符串拼接可以使用set
命令也可以使用list
命令。
使用set拼接
如果使用set进行字符串拼接,对应的命令格式如下:
1
2
set(var1 ${var1} ${var2} ...) #直接拼接
set(var1 "${var1} ${var2} ...") #可以使用空格隔开
关于上面的命令其实就是将从第二个参数开始往后所有的字符串进行拼接,最后将结果存储到第一个参数中,如果第一个参数中原来有数据会对原数据就行覆盖。
使用list拼接
如果使用list进行字符串拼接,对应的命令格式如下:
1
2
3
list(APPEND <list> [<element> ...])
list(APPEND var1 ${var1} ${var2})
list(APPEND var1 "${var1} ${var2} ...")
list
命令的功能比set
要强大,字符串拼接只是它的其中一个功能,所以需要在它第一个参数的位置指定出我们要做的操作,APPEND
表示进行数据追加,后边的参数和set
就一样了。
在CMake中,使用set
命令可以创建一个list
。一个在list
内部是一个由分号;
分割的一组字符串。例如,set(var a b c d e)
命令将会创建一个list:a;b;c;d;e
,但是最终打印变量值的时候得到的是abcde
。
1
2
3
4
set(var1 a;b;c;d;e)
set(var2 a b c d e)
message(${var1})
message(${var2})
输出的结果:
1
2
abcde
abcde
3.9.2 字符串移除
我们在通过file
搜索某个目录就得到了该目录下所有的源文件,但是其中有些源文件并不是我们所需要的,比如:
1
2
3
4
5
6
7
8
9
$ tree
.
├── add.cpp
├── div.cpp
├── main.cpp
├── mult.cpp
└── sub.cpp
0 directories, 5 files
在当前这么目录有五个源文件,其中main.cpp
是一个测试文件。如果我们想要把计算器相关的源文件生成一个动态库给别人使用,那么只需要add.cpp、div.cp、mult.cpp、sub.cpp
这四个源文件就可以了。此时,就需要将main.cpp
从搜索到的数据中剔除出去,想要实现这个功能,也可以使用list
。
1
list(REMOVE_ITEM <list> <value> [<value> ...])
通过上面的命令原型可以看到删除和追加数据类似,只不过是第一个参数变成了REMOVE_ITEM
。
举例:
1
2
3
4
5
6
7
8
9
10
cmake_minimum_required(VERSION 3.0)
project(TEST)
set(TEMP "hello,world")
file(GLOB SRC_1 ${PROJECT_SOURCE_DIR}/src/*.c)
# 移除前打印
message(STATUS "message: ${SRC_1}")
# 移除 main.cpp
list(REMOVE_ITEM SRC_1 ${PROJECT_SOURCE_DIR}/src/main.c)
# 移除后打印
message(STATUS "message: ${SRC_1}")
list其他功能:
-
获取 list 的长度。
1
list(LENGTH <list> <output variable>)
LENGTH
:子命令LENGTH用于读取列表长度<list>
:当前操作的列表<output variable>
:新创建的变量,用于存储列表的长度。
-
读取列表中指定索引的的元素,可以指定多个索引
1
list(GET <list> <element index> [<element index> ...] <output variable>)
-
<list>
:当前操作的列表 -
<element index>
:列表元素的索引
- 从0开始编号,索引0的元素为列表中的第一个元素;
- 索引也可以是负数,
-1
表示列表的最后一个元素,-2
表示列表倒数第二个元素,以此类推 - 当索引(不管是正还是负)超过列表的长度,运行会报错
-
<output variable>
:新创建的变量,存储指定索引元素的返回结果,也是一个列表。
-
-
将列表中的元素用连接符(字符串)连接起来组成一个字符串
1
list (JOIN <list> <glue> <output variable>)
<list>
:当前操作的列表<glue>
:指定的连接符(字符串)<output variable>
:新创建的变量,存储返回的字符串
-
查找列表是否存在指定的元素,若果未找到,返回-1
1
list(FIND <list> <value> <output variable>)
<list>
:当前操作的列表<value>
:需要在列表中搜索的元素<output variable>
:新创建的变量- 如果列表
<list>
中存在<value>
,那么返回<value>
在列表中的索引 - 如果未找到则返回-1。
- 如果列表
-
将元素追加到列表中
1
list (APPEND <list> [<element> ...])
-
在list中指定的位置插入若干元素
1
list(INSERT <list> <element_index> <element> [<element> ...])
-
将元素插入到列表的0索引位置
1
list (PREPEND <list> [<element> ...])
-
将列表中最后元素移除
1
list (POP_BACK <list> [<out-var>...])
-
将列表中第一个元素移除
1
list (POP_FRONT <list> [<out-var>...])
-
将指定的元素从列表中移除
1
list (REMOVE_ITEM <list> <value> [<value> ...])
-
将指定索引的元素从列表中移除
1
list (REMOVE_AT <list> <index> [<index> ...])
-
移除列表中的重复元素
1
list (REMOVE_DUPLICATES <list>)
-
列表翻转
1
list(REVERSE <list>)
-
列表排序
1
list (SORT <list> [COMPARE <compare>] [CASE <case>] [ORDER <order>])
COMPARE
:指定排序方法。有如下几种值可选:STRING
:按照字母顺序进行排序,为默认的排序方法FILE_BASENAME
:如果是一系列路径名,会使用basename进行排序NATURAL
:使用自然数顺序排序
CASE
:指明是否大小写敏感。有如下几种值可选:SENSITIVE
: 按照大小写敏感的方式进行排序,为默认值INSENSITIVE
:按照大小写不敏感方式进行排序
ORDER
:指明排序的顺序。有如下几种值可选:ASCENDING
:按照升序排列,为默认值DESCENDING
:按照降序排列
3.10 宏定义
在C/C++中可以使用宏定义编写代码进行测试或者避免重定义等功能。
#include <stdio.h>
#define NUMBER 3
int main()
{
int a = 10;
#ifdef DEBUG
printf("我是一个程序猿, 我不会爬树...\n");
#endif
for(int i=0; i<NUMBER; ++i)
{
printf("hello, GCC!!!\n");
}
return 0;
}
在程序的第七行对DEBUG
宏进行了判断,如果该宏被定义了,那么第八行就会进行日志输出,如果没有定义这个宏,就不会进入宏定义之间代码块,因此最终无法看到日志输入出(上述代码包括其引用的头文件中没有定义这个宏)。
为了让测试更灵活,我们可以不在代码中定义这个宏,而是在测试的时候去把它定义出来,其中一种方式就是在gcc/g++
命令中使用-D加宏的名字
去指定,如下:
1
$ gcc test.c -DDEBUG -o app
在CMake
中我们也可以做类似的事情,对应的命令叫做add_definitions
:
1
add_definitions(-D宏名称)
举例:
1
2
3
4
5
cmake_minimum_required(VERSION 3.0)
project(TEST)
# 自定义 DEBUG 宏
add_definitions(-DDEBUG)
add_executable(app ./test.c)
通过这种方式,上述代码中的第八行日志就能够被输出出来了。
3.11 CMake
中常用的宏
宏 | 功能 |
---|---|
PROJECT_SOURCE_DIR | 使用cmake命令后紧跟的目录,一般是工程的根目录 |
PROJECT_BINARY_DIR | 执行cmake命令的目录 |
CMAKE_CURRENT_SOURCE_DIR | 当前处理的CMakeLists.txt所在的路径 |
CMAKE_CURRENT_BINARY_DIR | target 编译目录 |
EXECUTABLE_OUTPUT_PATH | 重新定义目标二进制可执行文件的存放位置 |
LIBRARY_OUTPUT_PATH | 重新定义目标链接库文件的存放位置 |
PROJECT_NAME | 返回通过PROJECT指令定义的项目名称 |
CMAKE_BINARY_DIR | 项目实际构建路径,假设在build 目录进行的构建,那么得到的就是这个目录的路径 |
第二部分 项目构建
1、嵌套的CMake
如果项目很大,或者项目中有很多的源码目录,在通过CMake管理项目的时候如果只使用一个CMakeLists.txt
,那么这个文件相对会比较复杂,有一种化繁为简的方式就是给每个源码目录都添加一个CMakeLists.txt
文件(头文件目录不需要),这样每个文件都不会太复杂,而且更灵活,更容易维护。
先来看一下下面的这个的目录结构:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
$ tree
.
├── build
├── CMakeLists.txt
├── calc
│ ├── add.cpp
│ ├── CMakeLists.txt
│ ├── div.cpp
│ ├── mult.cpp
│ └── sub.cpp
├── include
│ ├── calc.h
│ └── sort.h
├── sort
│ ├── CMakeLists.txt
│ ├── insert.cpp
│ └── select.cpp
├── test1
│ ├── calc.cpp
│ └── CMakeLists.txt
└── test2
├── CMakeLists.txt
└── sort.cpp
6 directories, 15 files
include 目录
:头文件目录calc目录
:目录中的四个源文件对应的加、减、乘、除算法- 对应的头文件是
include
中的calc.h
- 对应的头文件是
sort目录
:目录中的两个源文件对应的是插入排序和选择排序算法- 对应的头文件是
include
中的sort.h
- 对应的头文件是
test1 目录
:测试目录,对加、减、乘、除算法进行测试test2 目录
:测试目录,对排序算法进行测试
可以看到各个源文件目录所需要的CMakeLists.txt
文件现在已经添加完毕了。接下来庖丁解牛,我们依次分析一下各个文件中需要添加的内容。
1.1 准备工作
1.1.1 节点关系
众所周知,Linux的目录是树状结构,所以嵌套的 CMake 也是一个树状结构,最顶层的 CMakeLists.txt 是根节点,其次都是子节点。
因此,我们需要了解一些关于 CMakeLists.txt
文件中变量作用域的一些信息:
- 根节点
CMakeLists.txt
中的变量全局有效 - 父节点
CMakeLists.txt
中的变量可以在子节点中使用 - 子节点
CMakeLists.txt
中的变量只能在当前节点中使用
1.1.2 添加子目录
嵌套 CMake 在构建项目时需要知道其父子节点之间的关系是如何建立的,这里需要用到一个 CMake 命令:
1
add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL])
source_dir
:指定了CMakeLists.txt
源文件和代码文件的位置,其实就是指定子目录binary_dir
:指定了输出文件的路径,一般不需要指定,忽略即可。EXCLUDE_FROM_ALL
:在子路径下的目标默认不会被包含到父路径的ALL
目标里,并且也会被排除在IDE工程文件之外。用户必须显式构建在子路径下的目标。
通过这种方式CMakeLists.txt
文件之间的父子关系就被构建出来了。
1.2 实际应用
在上面的目录中我们要做如下事情:
- 通过
test1 目录
中的测试文件进行计算器相关的测试 - 通过
test2 目录
中的测试文件进行排序相关的测试
现在相当于是要进行模块化测试,对于calc
和sort
目录中的源文件来说,可以将它们先编译成库文件(可以是静态库也可以是动态库)然后在提供给测试文件使用即可。库文件的本质其实还是代码,只不过是从文本格式变成了二进制格式。
1.2.1 根目录
根目录中的 CMakeLists.txt
文件内容如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
cmake_minimum_required(VERSION 3.0)
project(test)
# 定义变量
# 静态库生成的路径
set(LIB_PATH ${CMAKE_CURRENT_SOURCE_DIR}/lib)
# 测试程序生成的路径
set(EXEC_PATH ${CMAKE_CURRENT_SOURCE_DIR}/bin)
# 头文件目录
set(HEAD_PATH ${CMAKE_CURRENT_SOURCE_DIR}/include)
# 静态库的名字
set(CALC_LIB calc)
set(SORT_LIB sort)
# 可执行程序的名字
set(APP_NAME_1 test1)
set(APP_NAME_2 test2)
# 添加子目录
add_subdirectory(calc)
add_subdirectory(sort)
add_subdirectory(test1)
add_subdirectory(test2)
在根节点对应的文件中主要做了两件事情:定义全局变量
和添加子目录
。
- 定义的全局变量主要是给子节点使用,目的是为了提高子节点中的
CMakeLists.txt
文件的可读性和可维护性,避免冗余并降低出差的概率。 - 一共添加了四个子目录,每个子目录中都有一个
CMakeLists.txt
文件,这样它们的父子关系就被确定下来了。
1.2.2 calc 目录
calc 目录中的 CMakeLists.txt
文件内容如下:
1
2
3
4
5
6
cmake_minimum_required(VERSION 3.0)
project(CALCLIB)
aux_source_directory(./ SRC)
include_directories(${HEAD_PATH})
set(LIBRARY_OUTPUT_PATH ${LIB_PATH})
add_library(${CALC_LIB} STATIC ${SRC})
- 第3行
aux_source_directory
:搜索当前目录(calc目录)下的所有源文件添加到SRC变量中 - 第4行
include_directories
:包含头文件路径,HEAD_PATH
是在根节点文件中定义的 - 第5行
set
:设置库的生成的路径,LIB_PATH
是在根节点文件中定义的(LIBRARY_OUTPUT_PATH 是宏) - 第6行
add_library
:生成静态库,静态库名字CALC_LIB
是在根节点文件中定义的
1.2.3 sort 目录
sort 目录中的 CMakeLists.txt
文件内容如下:
1
2
3
4
5
6
cmake_minimum_required(VERSION 3.0)
project(SORTLIB)
aux_source_directory(./ SRC)
include_directories(${HEAD_PATH})
set(LIBRARY_OUTPUT_PATH ${LIB_PATH})
add_library(${SORT_LIB} SHARED ${SRC})
- 第6行
add_library
:生成动态库,动态库名字SORT_LIB
是在根节点文件中定义的
这个文件中的内容和calc
节点文件中的内容类似,只不过这次生成的是动态库。
在生成库文件的时候,这个库可以是静态库也可以是动态库,一般需要根据实际情况来确定。如果生成的库比较大,建议将其制作成动态库。
1.2.4 test1 目录
test1 目录中的 CMakeLists.txt
文件内容如下:
1
2
3
4
5
6
7
8
cmake_minimum_required(VERSION 3.0)
project(CALCTEST)
aux_source_directory(./ SRC)
include_directories(${HEAD_PATH})
link_directories(${LIB_PATH})
link_libraries(${CALC_LIB})
set(EXECUTABLE_OUTPUT_PATH ${EXEC_PATH})
add_executable(${APP_NAME_1} ${SRC})
- 第4行
include_directories
:指定头文件路径,HEAD_PATH
变量是在根节点文件中定义的 - 第6行
link_libraries
:指定可执行程序要链接的静态库
,CALC_LIB
变量是在根节点文件中定义的 - 第7行
set
:指定可执行程序生成的路径,EXEC_PATH
变量是在根节点文件中定义的 - 第8行
add_executable
:生成可执行程序,APP_NAME_1
变量是在根节点文件中定义的
此处的可执行程序链接的是静态库,最终静态库会被打包到可执行程序中,可执行程序启动之后,静态库也就随之被加载到内存中了。
1.2.5 test2 目录
test2 目录中的 CMakeLists.txt
文件内容如下:
1
2
3
4
5
6
7
8
cmake_minimum_required(VERSION 3.0)
project(SORTTEST)
aux_source_directory(./ SRC)
include_directories(${HEAD_PATH})
set(EXECUTABLE_OUTPUT_PATH ${EXEC_PATH})
link_directories(${LIB_PATH})
add_executable(${APP_NAME_2} ${SRC})
target_link_libraries(${APP_NAME_2} ${SORT_LIB})
- 第四行
include_directories
:包含头文件路径,HEAD_PATH
变量是在根节点文件中定义的 - 第五行
set
:指定可执行程序生成的路径,EXEC_PATH
变量是在根节点文件中定义的 - 第六行
link_directories
:指定可执行程序要链接的动态库的路径,LIB_PATH
变量是在根节点文件中定义的 - 第七行
add_executable
:生成可执行程序,APP_NAME_2
变量是在根节点文件中定义的 - 第八行
target_link_libraries
:指定可执行程序要链接的动态库的名字
在生成可执行程序的时候,动态库不会被打包到可执行程序内部。当可执行程序启动之后动态库也不会被加载到内存,只有可执行程序调用了动态库中的函数的时候,动态库才会被加载到内存中,且多个进程可以共用内存中的同一个动态库,所以动态库又叫共享库。
1.2.6 构建项目
一切准备就绪之后,开始构建项目,进入到根节点目录的build 目录
中,执行cmake 命令
,如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ cmake ..
-- The C compiler identification is GNU 5.4.0
-- The CXX compiler identification is GNU 5.4.0
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /home/robin/abc/cmake/calc/build
在build
目录中会生成了一些文件和目录,如下所示:
1
2
3
4
5
6
7
8
9
10
$ tree build -L 1
build
├── calc # 目录
├── CMakeCache.txt # 文件
├── CMakeFiles # 目录
├── cmake_install.cmake # 文件
├── Makefile # 文件
├── sort # 目录
├── test1 # 目录
└── test2 # 目录
然后在build 目录
下执行make 命令
,会得到以下结果。
- 在项目根目录的
lib
目录中生成了静态库libcalc.a
- 在项目根目录的
lib
目录中生成了动态库libsort.so
- 在项目根目录的
bin
目录中生成了可执行程序test1
- 在项目根目录的
bin
目录中生成了可执行程序test2
写在最后:
在项目中,如果将程序中的某个模块制作成了动态库或者静态库
并且在CMakeLists.txt 中指定了库的输出目录
,而后其它模块又需要加载这个生成的库文件,此时直接使用就可以了,如果没有指定库的输出路径或者需要直接加载外部提供的库文件,此时就需要使用 link_directories 将库文件路径指定出来。
2、流程控制
在 CMake 的 CMakeLists.txt 中也可以进行流程控制,也就是说可以像写 shell 脚本那样进行条件判断
和循环
。
2.1 条件判断
条件判断语法如下:
1
2
3
4
5
6
7
if(<condition>)
<commands>
elseif(<condition>)
<commands>
else()
<commands>
endif()
在进行条件判断的时候,如果有多个条件,那么可以写多个elseif
,最后一个条件可以使用else
,但是开始和结束是必须要成对出现的,分别为:if
和endif
。
2.1.1 基本表达式
基本表达式的判定方法除了一些宏之外与 C++ 一样。
1
if(<expression>)
如果是基本表达式,expression
有以下三种情况:常量
、变量
、字符串
。
- 如果是
1
,ON
,YES
,TRUE
,Y
,非零值
,非空字符串
时,条件判断返回True
- 如果是
0
,OFF
,NO
,FALSE
,N
,IGNORE
,NOTFOUND
,空字符串
时,条件判断返回False
2.1.3 逻辑判断
NOT
1
if(NOT <condition>)
逻辑非:如果条件condition
为True
将返回False
,如果条件condition
为False
将返回True
。
AND
1
if(<cond1> AND <cond2>)
逻辑与:如果cond1
和cond2
同时为True
,返回True
否则返回False
。
OR
1
if(<cond1> OR <cond2>)
逻辑或:如果cond1
和cond2
两个条件中至少有一个为True
,返回True
,如果两个条件都为False
则返回False
。
2.1.3 比较
基于数值的比较
1
2
3
4
5
if(<variable|string> LESS <variable|string>)
if(<variable|string> GREATER <variable|string>)
if(<variable|string> EQUAL <variable|string>)
if(<variable|string> LESS_EQUAL <variable|string>)
if(<variable|string> GREATER_EQUAL <variable|string>)
LESS
:如果左侧数值小于
右侧,返回True
GREATER
:如果左侧数值大于
右侧,返回True
EQUAL
:如果左侧数值等于
右侧,返回True
LESS_EQUAL
:如果左侧数值小于等于
右侧,返回True
GREATER_EQUAL
:如果左侧数值大于等于
右侧,返回True
基于字典序的比较(将两个字符串逐字符比较)
1
2
3
4
5
if(<variable|string> STRLESS <variable|string>)
if(<variable|string> STRGREATER <variable|string>)
if(<variable|string> STREQUAL <variable|string>)
if(<variable|string> STRLESS_EQUAL <variable|string>)
if(<variable|string> STRGREATER_EQUAL <variable|string>)
STRLESS
:如果左侧字符串小于
右侧,返回True
STRGREATER
:如果左侧字符串大于
右侧,返回True
STREQUAL
:如果左侧字符串等于
右侧,返回True
STRLESS_EQUAL
:如果左侧字符串小于等于
右侧,返回True
STRGREATER_EQUAL
:如果左侧字符串大于等于
右侧,返回True
2.1.4 文件操作
-
判断文件或者目录是否存在
1
if(EXISTS path-to-file-or-directory)
如果文件或者目录存在返回
True
,否则返回False
。 -
判断是不是目录
1
if(IS_DIRECTORY path)
- 此处目录的 path 必须是绝对路径
- 如果目录存在返回
True
,目录不存在返回False
-
判断是不是软连接
1
if(IS_SYMLINK file-name)
- 此处的 file-name 对应的路径必须是绝对路径
- 如果软链接存在返回
True
,软链接不存在返回False
。 - 软链接相当于 Windows 里的快捷方式
-
判断是不是绝对路径
1
if(IS_ABSOLUTE path)
- 关于绝对路径:
- 如果是
Linux
,该路径需要从根目录开始描述 - 如果是
Windows
,该路径需要从盘符开始描述
- 如果是
- 如果是绝对路径返回
True
,如果不是绝对路径返回False
。
- 关于绝对路径:
2.1.5 其它
-
判断某个元素是否在列表中
1
if(<variable|string> IN_LIST <variable>)
- CMake 版本要求:大于等于3.3
- 如果这个元素在列表中返回
True
,否则返回False
。
-
比较两个路径是否相等
1
if(<variable|string> PATH_EQUAL <variable|string>)
- CMake 版本要求:大于等于3.24
- 如果这个元素在列表中返回
True
,否则返回False
。
关于路径的比较其实就是另个字符串的比较,如果路径格式书写没有问题也可以通过下面这种方式进行比较:
1
if(<variable|string> STREQUAL <variable|string>)
我们在书写某个路径的时候,可能由于误操作会多写几个分隔符,比如把
/a/b/c
写成/a//b///c
,此时通过STREQUAL
对这两个字符串进行比较肯定是不相等的,但是通过PATH_EQUAL
去比较两个路径,得到的结果确实相等的,可以看下面的例子:1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
cmake_minimum_required(VERSION 3.26) project(test) if("/home//robin///Linux" PATH_EQUAL "/home/robin/Linux") message("路径相等") else() message("路径不相等") endif() message(STATUS "@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@") if("/home//robin///Linux" STREQUAL "/home/robin/Linux") message("路径相等") else() message("路径不相等") endif()
输出的日志信息如下:
1 2 3
路径相等 @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ 路径不相等
通过得到的结果我们可以得到一个结论:在进行路径比较的时候,如果使用 PATH_EQUAL 可以自动剔除路径中多余的分割线然后再进行路径的对比,使用 STREQUAL 则只能进行字符串比较。
2.2 循环
在 CMake 中循环有两种方式,分别是:foreach
和while
。
2.2.1 foreach
使用 foreach 进行循环,语法格式如下:
1
2
3
foreach(<loop_var> <items>)
<commands>
endforeach()
通过foreach
我们就可以对items
中的数据进行遍历,然后通过loop_var
将遍历到的当前的值取出,在取值的时候有以下几种用法:
方法1
1
foreach(<loop_var> RANGE <stop>)
RANGE
:关键字,表示要遍历范围stop
:这是一个正整数,表示范围的结束值,在遍历的时候从 0 开始,最大值为 stop。loop_var
:存储每次循环取出的值
举例:
1
2
3
4
5
6
cmake_minimum_required(VERSION 3.2)
project(test)
# 循环
foreach(item RANGE 10)
message(STATUS "当前遍历的值为: ${item}" )
endforeach()
输出:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ cmake ..
-- 当前遍历的值为: 0
-- 当前遍历的值为: 1
-- 当前遍历的值为: 2
-- 当前遍历的值为: 3
-- 当前遍历的值为: 4
-- 当前遍历的值为: 5
-- 当前遍历的值为: 6
-- 当前遍历的值为: 7
-- 当前遍历的值为: 8
-- 当前遍历的值为: 9
-- 当前遍历的值为: 10
-- Configuring done
-- Generating done
-- Build files have been written to: /home/robin/abc/a/build
强调:在对一个整数区间进行遍历的时候,得到的范围是这样的 [0,stop],右侧是闭区间包含 stop 这个值。
方法2
1
foreach(<loop_var> RANGE <start> <stop> [<step>])
这是上面方法1
的加强版,我们在遍历一个整数区间的时候,除了可以指定起始范围,还可以指定步长。
RANGE
:关键字,表示要遍历范围start
:这是一个正整数,表示范围的起始值,也就是说最小值为 startstop
:这是一个正整数,表示范围的结束值,也就是说最大值为 stopstep
:控制每次遍历的时候以怎样的步长增长,默认为1,可以不设置loop_var
:存储每次循环取出的值
举例:
1
2
3
4
5
6
cmake_minimum_required(VERSION 3.2)
project(test)
foreach(item RANGE 10 30 2)
message(STATUS "当前遍历的值为: ${item}" )
endforeach()
输出:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$ cmake ..
-- 当前遍历的值为: 10
-- 当前遍历的值为: 12
-- 当前遍历的值为: 14
-- 当前遍历的值为: 16
-- 当前遍历的值为: 18
-- 当前遍历的值为: 20
-- 当前遍历的值为: 22
-- 当前遍历的值为: 24
-- 当前遍历的值为: 26
-- 当前遍历的值为: 28
-- 当前遍历的值为: 30
-- Configuring done
-- Generating done
-- Build files have been written to: /home/robin/abc/a/build
强调:在使用上面的方式对一个整数区间进行遍历的时候,得到的范围是这样的 [start,stop],左右两侧都是闭区间,包含 start 和 stop 这两个值,步长 step 默认为1,可以不设置。
方法3
1
foreach(<loop_var> IN [LISTS [<lists>]] [ITEMS [<items>]])
这是foreach
的另一个变体,通过这种方式我们可以对更加复杂的数据进行遍历,前两种方式只适用于对某个正整数范围内的遍历。
IN
:关键字,表示在 xxx 里边LISTS
:关键字,对应的是列表list
,通过set、list
可以获得ITEMS
:关键字,对应的也是列表loop_var
:存储每次循环取出的值
举例:
LISTS 方式
1
2
3
4
5
6
7
8
9
cmake_minimum_required(VERSION 3.2)
project(test)
# 创建 list
set(WORD a b c d)
set(NAME ace sabo luffy)
# 遍历 list
foreach(item IN LISTS WORD NAME)
message(STATUS "当前遍历的值为: ${item}" )
endforeach()
在上面的例子中,创建了两个 list
列表,在遍历的时候对它们两个都进行了遍历(可以根据实际需求选择同时遍历多个或者只遍历一个)。
输出:
1
2
3
4
5
6
7
8
9
10
11
12
$ cd build/
$ cmake ..
-- 当前遍历的值为: a
-- 当前遍历的值为: b
-- 当前遍历的值为: c
-- 当前遍历的值为: d
-- 当前遍历的值为: ace
-- 当前遍历的值为: sabo
-- 当前遍历的值为: luffy
-- Configuring done
-- Generating done
-- Build files have been written to: /home/robin/abc/a/build
ITEMS 方式
1
2
3
4
5
6
7
8
cmake_minimum_required(VERSION 3.2)
project(test)
set(WORD a b c "d e f")
set(NAME ace sabo luffy)
foreach(item IN ITEMS ${WORD} ${NAME})
message(STATUS "当前遍历的值为: ${item}" )
endforeach()
在上面的例子中,遍历过程中将关键字LISTS
改成了ITEMS
,后边跟的还是一个或者多个列表,只不过此时需要通过${}
将列表中的值取出。其输出的信息和上一个例子是一样的。
小细节:在通过 set 组织列表的时候,如果某个字符串中有空格,可以通过双引号将其包裹起来,具体的操作方法可以参考上面的例子。
方式 4
注意事项:这种循环方式要求CMake的版本大于等于 3.17。
1
foreach(<loop_var>... IN ZIP_LISTS <lists>)
通过这种方式,遍历的还是一个或多个列表,可以理解为是方式3的加强版。因为通过上面的方式遍历多个列表,但是又想把指定列表中的元素取出来使用是做不到的,在这个加强版中就可以轻松实现。
loop_var
:存储每次循环取出的值,可以根据要遍历的列表的数量指定多个变量,用于存储对应的列表当前取出的那个值。- 如果指定了多个变量名,它们的数量应该和列表的数量相等
- 如果只给出了一个 loop_var,那么它将一系列的 loop_var_N 变量来存储对应列表中的当前项,也就是说 loop_var_0 对应第一个列表,loop_var_1 对应第二个列表,以此类推……
- 如果遍历的多个列表中一个列表较短,当它遍历完成之后将不会再参与后续的遍历。
IN
:关键字,表示在 xxx 里边ZIP_LISTS
:关键字,对应的是列表list
,通过set 、list
可以获得
举例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
cmake_minimum_required(VERSION 3.17)
project(test)
# 通过list给列表添加数据
list(APPEND WORD hello world "hello world")
list(APPEND NAME ace sabo luffy zoro sanji)
# 遍历列表
foreach(item1 item2 IN ZIP_LISTS WORD NAME)
message(STATUS "当前遍历的值为: item1 = ${item1}, item2=${item2}" )
endforeach()
message("=============================")
# 遍历列表
foreach(item IN ZIP_LISTS WORD NAME)
message(STATUS "当前遍历的值为: item1 = ${item_0}, item2=${item_1}" )
endforeach()
在这个例子中关于列表数据的添加是通过list
来实现的。在遍历列表的时候一共使用了两种方式,一种提供了多个变量来存储当前列表中的值,另一种只有一个变量,但是实际取值的时候需要通过变量名_0、变量名_1、变量名_N
的方式来操作,注意事项:第一个列表对应的编号是0,第一个列表对应的编号是0,第一个列表对应的编号是0。
输出:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$ cd build/
$ cmake ..
-- 当前遍历的值为: item1 = hello, item2=ace
-- 当前遍历的值为: item1 = world, item2=sabo
-- 当前遍历的值为: item1 = hello world, item2=luffy
-- 当前遍历的值为: item1 = , item2=zoro
-- 当前遍历的值为: item1 = , item2=sanji
=============================
-- 当前遍历的值为: item1 = hello, item2=ace
-- 当前遍历的值为: item1 = world, item2=sabo
-- 当前遍历的值为: item1 = hello world, item2=luffy
-- 当前遍历的值为: item1 = , item2=zoro
-- 当前遍历的值为: item1 = , item2=sanji
-- Configuring done (0.0s)
-- Generating done (0.0s)
-- Build files have been written to: /home/robin/abc/a/build
2.2.2 while
除了使用 foreach
也可以使用 while
进行循环,关于循环结束对应的条件判断的书写格式和 if/elseif
是一样的。while
的语法格式如下:
1
2
3
while(<condition>)
<commands>
endwhile()
while
循环比较简单,只需要指定出循环结束的条件即可:
举例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
cmake_minimum_required(VERSION 3.5)
project(test)
# 创建一个列表 NAME
set(NAME luffy sanji zoro nami robin)
# 得到列表长度
list(LENGTH NAME LEN)
# 循环
while(${LEN} GREATER 0)
message(STATUS "names = ${NAME}")
# 弹出列表头部元素
list(POP_FRONT NAME)
# 更新列表长度
list(LENGTH NAME LEN)
endwhile()
输出:
1
2
3
4
5
6
7
8
9
10
$ cd build/
$ cmake ..
-- names = luffy;sanji;zoro;nami;robin
-- names = sanji;zoro;nami;robin
-- names = zoro;nami;robin
-- names = nami;robin
-- names = robin
-- Configuring done (0.0s)
-- Generating done (0.0s)
-- Build files have been written to: /home/robin/abc/a/build
可以看到当列表中的元素全部被弹出之后,列表的长度变成了0,此时while
循环也就退出了。