[26/3/20]CMake入门
参考资料:
【现代C++: CMake简明教程】www.bilibili.com/video/BV1xa…
【CMake 保姆级教程【C/C++】】 www.bilibili.com/video/BV14s…
初次使用
- 安装 cmake:
sudo apt install cmake - 检查安装结果:
cmake --version - 创建一个 cpp 文件,并写源码。假设这个文件名为:
hello.cpp - 在 cpp 项目目录下创建一个空文本文件,名字写成:
CMakeLists.txt - 在这个 txt 文件里写以下内容:
# 指定最低的cmake工具版本号
cmake_minimum_required(VERSION 3.20)
# 指定本工程的工程名字
project(my_project)
# 指定编译结果文件名 以及用什么文件来编译
add_executable(hello_app hello.cpp)
- 运行 cmake:
cmake --build build - 运行编译后的程序,验证编译成功(注意在 build 文件夹下):
./build/hellp_app
构建指令说明
对于构建:一般来说是分成两步的,先用 CMake 生成 makefile,再用 make 构建。
所以传统 CMake 的操作流程为:
mkdir build; cd build; cmake ..; make
但是这四条命令太繁琐了,有更加现代的语法进行替换:
cmake -B build:CMake 帮你创建 build 目录并构建 makefilecmake --build build:CMake 帮你调用 make 进行构建
多文件编译
如果是多文件,那么 add_executable(输出文件名 源文件名列表)
这里的源文件名列表,需要把所有的源代码文件列出来,你可以采用空格 ``进行分隔也可以采用分号 ; 进行分隔。
(这和 g++ *.cpp -o my_app 有异曲同工之妙)
但是手写所有的文件名太累了,所以可以采用文件搜索功能!
编译命令
首先,由于 CMakeLists.txt 这个文件名是被强制要求的,所以 CMake 永远会对着这个文件名去运行,所以你不需要把这个名字作为参数输入。
然后,假设你当前的工作文件夹下就有这个文件,你可以直接使用 cmake . 来进行编译。
但是这会使得所有的编译过程文件全都出现在当前文件夹下,所以你还可以这样:
mkdir build 接着 cd build 最后 cmake .. 就是告诉 CMake 在哪个目录下找这个文件。
最后要运行 make 来编译(前面只是使用 CMake 生成 Makefile)
CMake 语言:变量
set
- 设置变量:
set(变量名 变量内容):CMake 的变量默认都是字符串,且和 Shell 类似,需要使用${val_name}来访问
- 变量合并:可以这样把 b 变量的内容追加到 a 中:
set(a a b)
实例:
# 设置变量,注意后面的多个文件名会合并成一个字符串!
set(SRC_LIST main.cpp a.cpp b.cpp c.cpp) # 也可以用;分隔
add_executable(myApp ${SRC_LIST}) # 别忘了${}
# 通过设置 宏变量 来指定C++标准,可以是11/14/17/20...
set(CMAKE_CXX_STANDARD 11)
对于设置 C++标准,你还可以这样:
cmake CMakeLists.txt -DCMAKE_CXX_STANDARD=11
请注意,这里的 -D 表示设置宏,这条命令和直接写入到文件中效果等价
文件搜索
aux_source_directory(<dir> variable):把指定路径下搜索到的源文件存储到变量里(自动检索 .c 和 .cpp 这两类文件)file(GLOB/GLOB_RECURES 变量名 要找的目录和文件类型):
-
- GLOB:只找当前目录
- GLOB_RECURES:递归搜索
- 可以使用通配符
- 宏:
${PROJECT_SOURCE_DIR}:这个宏指代的是 整个项目最上层的 CMakeLists.txt 所在的路径,在指定路径时,可以使用这个宏变量来辅助 - 宏:
${CMAKE_CURRENT_SOURCE_DIR}:当前的 CMakeLists.txt 所在的路径
指定头文件路径
- 可以使用
tree命令先查看项目结构,然后推荐的项目结构可能如下:
.
├── CMakeLists.txt
├── build
├── include
│ └── head.h
└── src
├── a.cpp
├── b.cpp
├── c.cpp
└── main.cpp
- 此时由于 main.cpp 和 .h 文件不在同一级目录,直接编译会显示找不到头文件
include_directories(headpath):用这条命令设置头文件路径
库相关
文件后缀
基础概念:文件名
| 平台 | 动态库 | 静态库 |
|---|---|---|
| Linux | libxxx.so | libxxx.a |
| Win | libxxx.dll | libxxx.lib |
制作库相关命令
首先,由于是制作 库 而不是可执行程序,
所以,不需要指定可执行文件的输出路径,
也不需要指定输出可执行文件,
也就是不需要写 add_executable
此外,还需要注意的是,不要把 main.cpp 加入到源文件列表里!
因为库文件就是没有主函数的!
(比如说,可以把 main.cpp 移动到 src 文件夹之外)
- 制作动态库(共享库):
add_library(库的名称 SHARED 源文件列表)
- 制作静态库命令:
add_library(库的名称 STATIC 源文件列表)
- 执行
cmake和make命令之后,就会生成库文件了 - 库在发布时,必须同时将 生成的库文件和相应的头文件 一起发布!
(也可以注意到,源文件列表只包含 .cpp 文件,不需要 .h 文件!)
- 补充:指定库文件的生成路径:宏
${LIBRARY_OUTPUT_PATH}
链接静态库
语法:link_libraries(静态库列表)
注意:列表里可以写文件全名,也可以掐头(lib)去尾(.a)只写中间部分
如果找不到,还需要指定路径:(这个对动态和静态库都生效)
语法:link_directories(库目录列表)
最后:由于静态库是最终被编译进二进制文件的,所以说,后续使用就非常方便,除了要加上述两行指令之外,使用起来和纯源代码编译效果是一样的。
链接动态库
由于动态库需要在运行时被加载,所以要考虑
语法:target_link_libraries(需要库的文件名 动态库列表) 也可以掐头去尾
然后注意啊,由于动态库是运行时才被链接的,所以!
其中,需要库的文件名可以是可执行程序的名称,也可以是其他库
(就是其他库对后面的动态库有依赖的情况)
(这也是为什么链接指令写在生成可执行文件指令之后)
静态库的链接指令需要写在 add_executable 之前,
动态库的链接指令需要写在 add_executalbe 之后!
所以 target_link_libraries 需要被写在最后。
为啥编译的时候没有动态库还能编译成功?这不是有头文件嘛。
消息打印
语法:message([消息等级] "消息内容") 可以打印变量
消息等级参数:
- 不写:重要消息
STATUS:非重要消息(只有这个是标准输出)WARNING:警告,但是会继续执行AUTHOR_WARNING:警告(dev),但是会继续执行SEND_ERROR:错误,会继续执行,但是会跳过生成步骤FATAL_ERROR:错误,终止所有处理过程
字符串操作
- 字符串拼接:
set(变量名 ${拼接变量1} ${拼接变量2} ...)# 同理可以使用分号;分隔(也可以直接写字符串,不一定必须是变量) - 追加:
list(APPEND 变量名 要追加到后面的变量列表) - 移除:
list(REMOVE_ITEM 变量名 要删除的变量列表)(要完全匹配!!!) - 获取元素个数:
list(LENGTH 要计算长度的变量列表 接收长度变量名) - 获取指定索引元素:
list(GET 变量列表 索引值 接收数据的变量名) - 用指定连接符连接:
list(JOIN 变量列表 分隔符 合并后的变量名) - 查找元素索引:
list(FIND 变量列表 查找元素 输出变量名)# 没有就返回-1 - 插入若干元素:
list(INSERT 变量列表 插入位置索引 插入值列表) - 插入头部:
list(REPEND 变量列表 插入的元素列表) - 移除最后元素:
list(POP_BACK 变量列表 [存弹出的变量名]) - 移除头部元素:
list(POP_FRONT 变量列表 [存弹出的变量名]) - 移除指定索引元素:
list(REMOVE_AT 变量列表 索引列表) - 移除重复元素:
list(REMOVE_DUPLICATES 变量列表) - 列表翻转:
list(REVERSE 变量列表) - 列表排序:
list(SORT 变量列表 一些可选参数)
自定义宏
这里的自定义宏是写在 CMakeLists.txt 的,并且在程序源码里,可以用
#ifdef 来读取这些宏是否被定义。
定义宏语法:add_definitions(-D宏名字) # 可以定义多个,都是 -D 宏名字 并用空格分开就行
嵌套 CMake
嵌套规则
- 最核心的语法是,增加子节点(子目录):
add_subdirectory(子文件夹名) - 项目的根目录底下有一个 CMakeLists.txt 编译项目的时候只需要编译这一个就行了,让这个一个去调用各个子层级的 CMakeLists.txt
- 对于子层级,一般是,一个库(或一个 mian 项目,也就是要生成一个可执行程序的目录)底下有一个单独的 CMakeLists.txt 用来单独编译这个库(或程序)
- 父级的 CMakeLists.txt 中定义的变量可以被子层级调用,反之不行
实例
0) 项目树
项目结构如下,包含两个库 labc 和 lxyz,以及生成两个可执行程序 test1 和 test2:
.
├── CMakeLists.txt
├── include
│ ├── labc.h
│ └── lxyz.h
├── labc
│ ├── CMakeLists.txt
│ ├── a.cpp
│ ├── b.cpp
│ └── c.cpp
├── lxyz
│ ├── CMakeLists.txt
│ ├── x.cpp
│ ├── y.cpp
│ └── z.cpp
├── test1
│ ├── CMakeLists.txt
│ └── main.cpp
└── test2
├── CMakeLists.txt
└── main.cpp
1) 根目录 CMake 内容
对于主目录下的 CMake 文件,内容如下:
# 1. CMake 最小版本限制
cmake_minimum_required(VERSION 3.20)
# 2. 项目名称
project(myApp)
message("当前项目路径: ${PROJECT_SOURCE_DIR}")
# 定义变量用于后续操作(主要是为了被子文件调用)
# 1 设置静态库编译后的输出路径为./lib
set(LIB_PATH ${PROJECT_SOURCE_DIR}/lib)
# 2 设置可执行程序编译后的输出路径为./bin
set(EXEC_PATH ${PROJECT_SOURCE_DIR}/bin)
# 3 设置头文件的查找路径(要和你的实际路径保持一致!)
set(HEAD_PATH ${PROJECT_SOURCE_DIR}/include)
# 4 所调用的库的名字,也用变量保存(后续被子文件调用)
set(LABC_LIB labc)
set(LXYZ_LIB lxyz)
# 5 要构建的可执行程序的名字,也用变量保存(后续被子文件调用)
set(APP_NAME_1 myApp1)
set(APP_NAME_2 myApp2)
# 给当前节点增加子节点! 传入的是子文件夹的名字!
add_subdirectory(labc)
add_subdirectory(lxyz)
add_subdirectory(test1)
add_subdirectory(test2)
以后可以根据需要新增内容。
2) 库目录 CMake 内容
cmake_minimum_required(VERSION 3.20)
project(labc)
# 搜索源文件到变量SRC
# 由于这个cmake文件和源码放在同一目录所以用./
aux_source_directory(./ SRC)
# 指定头文件路径,这里调用了父节点变量
include_directories(${HEAD_PATH})
# 设置生成的库文件输出位置,采用宏变量和父节点变量
set(LIBRARY_OUTPUT_PATH ${LIB_PATH})
# 这是个库,设置生成库的名字和源文件,名字用父节点变量
add_library(${LABC_LIB} STATIC ${SRC})
对于 lxyz,只需要修改第 2 行的 labc 和第 12 行就行 LABC_LIB 就可以了。
3) 子项目 CMake 内容
cmake_minimum_required(VERSION 3.20)
project(test1)
# 搜索源代码路径,存入变量SRC
aux_source_directory(./ SRC)
# 指定头文件路径,用父节点变量
include_directories(${HEAD_PATH})
# 想要调用库,只有库的名字不一定找得到,所以还需要库路径
# - 指定库文件路径,用父节点变量
link_directories(${LIB_PATH})
# - 指定依赖库名字,用父节点变量
link_libraries(${LABC_LIB})
link_libraries(${LXYZ_LIB})
# 设置生成可执行文件的路径(宏变量),用父节点变量
set(EXECUTABLE_OUTPUT_PATH ${EXEC_PATH})
# 指定生成可执行程序
add_executable(${APP_NAME_1} ${SRC})
对于 test2,只需要修改第 2 行的 test1 和第 18 行的 APP_NAME_1 就可以了。
4) 构建命令
在项目根目录执行
cmake -B buildcmake --build build
在静态库中链接静态库
这个非常简单,就是比如说 main.cpp 依赖库 XYZ,库 XYZ 又依赖静态库 aaa,即:
main.cpp <- XYZ <- aaa
那么在 main.cpp 的 CMake 脚本下,只需要链接静态库 XYZ,不需要考虑 aaa;
而在 XYZ 的 CMake 脚本下,需要指明链接静态库 aaa;
然后你想要链接一个静态库,只需要同时指明这个库的名字和路径就行,也就是:
link_libraries(静态库的名字)link_directories(静态库所在目录)
最后最坑的一点是,总的来说,你还是需要编译库 aaa 的,
所以你需要把 aaa 所在的子目录加到编译列表里,也就是:
add_subdirectory(静态库所在的文件夹名)
并且这一行内容要在父级文件夹的 CMake 脚本中
在静态库中链接动态库
其实也是类似的,只是用的命令不一样,而且命令的位置有要求:
- 先指明库路径:
link_directories(动态库所在目录) - 链接库:
target_link_libraries(目标名字 动态库名字)
这里需要注意,目标名字是静态库名字,且链接库这行代码必须
写在 add_library 也就是生成当前的静态库指令的后面。
此外同样的,不要忘了把动态库所在的目录加入编译列表:
add_subdirectory(动态库所在的文件夹名)