编译环境:在 Wiondows 下,使用 VScode+WSL2+Ubuntu20.04+GCC 编译器
# 核心命令
# 项目配置
-
cmake_minimum_required指定CMake的最小版本要求
cmake_minimum_required(VERSION 3.10) |
project 定义项目名称,并可以指定项目使用的编程语言、web 主页地址、项目版本号等
project(MyProject VERSION 1.0 LANGUAGES CXX) |
-
set设置变量
#方式 1 各源文件剪枝空格间隔,并赋值给 SOURCE(变量名任取,不要重复即可)变量 | |
set(SOURCES main.cpp foo.cpp bar.cpp) | |
#方式 2 各源文件 “;” 间隔 | |
set(SOURCES main.cpp;foo.cpp;bar.cpp) | |
# 使用变量,采用 ${} 方式 | |
add_executable(MyExecutable ${SOURCES}) |
- 指定使用
c++标准
命令行编译时使用 c++ 标准,通过 -std=c++11 格式指定
g++ -std=c++11 -o demo main.cpp |
- 通过
set命令指定
CMake 文件指定 c++ 标准
set(CMAKE_CXX_STANDARD 11) |
- 通过
project命令指定
project(MyProject VERSION 1.0 LANGUAGES CXX CXX_STANDARD 11) |
-
C++标准对应有宏-DCMAKE_CXX_STANDARD,通过cmake命令指定
cmake -DCMAKE_CXX_STANDARD=11 .. # 注意,.. 表示上一级目录,指定到 CMakeLists.txt 所在的目录 |
| 变量名 | 描述 |
|---|---|
| CMAKE_SOURCE_DIR | CMakeLists.txt 所在的目录 |
| CMAKE_BINARY_DIR | 构建目录 |
| CMAKE_CURRENT_SOURCE_DIR | 当前 CMakeLists.txt 所在的目录 |
| CMAKE_CURRENT_BINARY_DIR | 当前构建目录 |
| CMAKE_CURRENT_LIST_DIR | 当前 CMakeLists.txt 所在的目录 |
| PROJECT_SOURCE_DIR | 表示项目根目录 |
| PROJECT_BINARY_DIR | 表示项目构建目录 |
| PROJECT_NAME | 表示项目名称 |
| CMAKE_CXX_STANDARD | 表示 C++ 标准 |
# 文件管理
- 指定输出路径
在 CMake 中指定可执行程序输出的路径,也有对应的宏,叫做 EXECUTABLE_OUTPUT_PATH ,可通过 set 命令指定,例如:
set(HOME /home/zhang/Linux) | |
set(EXECUTABLE_OUTPUT_PATH ${HOME}/bin) |
- 第一行:定义一个变量存储一个路径
- 第二行:将拼接好的路径赋值给
EXECUTABLE_OUTPUT_PATH变量
- 搜索文件
aux_source_group 查找 某路径 下的 所有源文件
aux_source_group("Source Files" variable) |
Source Files:表示搜索的文件所在的目录名称variable:将Source Files目录下搜索到的源文件列表赋值给variable变量
-
aux_source_group查找文件
# aux_source_group(${PROJECT_SOURCE_DIR}/src SRC_LIST) | |
aux_source_group(${CMAKE_CURRENT_SOURCE_DIR}/src SRC_LIST) |
PROJECT_SOURCE_DIR:表示项目根目录CMAKE_CURRENT_SOURCE_DIR:表示当前CMakeLists.txt所在的目录
搜索CMakeLists.txt所在的目录src下所有源文件,并将结果赋值给SRC_LIST变量。
-
file查找文件
使用file命令,查找某路径下的某格式文件
file(GLOB/GLOB_RECURSE 变量名 要搜索文件路径和文件类型) |
GLOB:表示搜索指定目录下所有文件,将满足条件的文件列表赋值给变量GLOB_RECURSE:表示递归搜索指定目录下所有文件,将满足条件的文件列表赋值给变量
- 包含头文件
在编译项目源文件时,需要将源文件对应的头文件包含进来,cmake 提供了include_directories命令,用于指定头文件的搜索路径:
include_directories(headpath) |
# 构建控制
# 编译选项
在 CMake 中,可以通过 option 命令来定义编译选项,例如:
option(USE_OPENMP "Use OpenMP for parallelization" ON) |
USE_OPENMP:表示编译选项的名称Use OpenMP for parallelization:表示编译选项的描述
# 链接控制
在 CMake 中,可以通过 target_link_libraries 命令来指定链接库,例如:
target_link_libraries(<TARGET> PRIVATE MyLibrary) |
TARGET:需要链接库的目标PRIVATE:表示链接库的作用域,可以是PRIVATE、PUBLIC、INTERFACE之一
# 工程实践
# 标准结构
# CMakeLists.txt | |
cmake_minimum_required(VERSION 3.10) | |
project(MyProject LANGUAGES CXX) | |
# 添加源文件 | |
set(SOURCES main.cpp foo.cpp bar.cpp) | |
add_executable(MyExecutable ${SOURCES}) | |
# 添加头文件搜索路径 | |
include_directories(${CMAKE_SOURCE_DIR}/include) | |
# 添加链接库 | |
target_link_libraries(MyExecutable PRIVATE MyLibrary) |
# 构建流程
mkdir build | |
cd build | |
cmake .. | |
make |
# 动静态库
# 生成静态库
add_library 命令用于创建库,其语法如下:
# 静态库 | |
add_library(MyLibrary STATIC [源文件1] [源文件2]) | |
# 动态库 | |
add_library(MyLibrary SHARED [源文件1] [源文件2]) |
MyLibrary 为生成 动静态 的 库名 ,完整动静态文件名包含三部分: lib + 库名 + 扩展名
SHARED :表示生成动态库,动态库扩展名为 .so (Linux)、 dll (Windows)
STATIC :表示生成静态库,静态库扩展名为 .a (Linux)、 lib (Windows)
静态库 默认不具有可执行权限,所以指定静态库路径的时候不可以使用 EXECUTABLE_OUTPUT_PATH ,而应该使用 LIBRARY_OUTPUT_PATH ,这个宏对于动态库和静态库都有效。
| 类别 | 编译 |
|---|---|
| 静态库 | 编译时链接 |
| 动态库 | 运行时链接 |
# 使用动静态库
链接动静态库,需要使用 link_libraries 命令
link_libraries(<static lib> [<static lib>...]) |
参数1 指定要链接静态库的名字,可以是文件全名,也可以是文件名(不带 lib 前缀和扩展名 .a )
参数2 要链接的其他静态库的名字
** 注意:** 如果该静态库不是系统提供的(自己制作或第三方提供静态库)可能出现找不到的情况,需要指定静态库的路径
link_directories(<path>) |
参数 指定静态库的路径
target_link_libraries 用于将动态库链接到目标可执行文件(因此位于 add_executable 或 add_library 命令之后)
target_link_libraries(<target> [PRIVATE|PUBLIC|INTERFACE] <item>...) |
<target> :表示目标可执行文件或库的名称
[PRIVATE|PUBLIC|INTERFACE] :表示链接库的访问权限,默认为 PUBLIC , 可以是 PRIVATE 、 PUBLIC 、 INTERFACE 之一
<item> :表示要链接的库的名称,可以是库的名称,也可以是库的路径
动态库 :具有传递性,即链接动态库的库,也会链接动态库所依赖的动态库,例如:如果 动态库A 链接 动态库B、C , 动态库D 链接 动态库A ,此时链接 动态库D 相当于链接了 动态库B、C 中定义的方法。
target_link_libraries(A B C) | |
target_link_libraries(D A) |
| 权限 | 描述 |
|---|---|
PRIVATE |
private 后面的库仅被 link 前面的 target 中,并且终结掉,第三方库不能感知 |
PUBLIC |
public 后面的库会被 link 到前面的 target 中,并且里面的符号会被导出,提供给第三方 |
INTERFACE |
interface 后面引入的库不会被连接到前面的 target 中,只会导出符号 |
# 进阶技巧
# 条件判断
if
if (EXISTS ${CMAKE_SOURCE_DIR}/include) | |
include_directories(${CMAKE_SOURCE_DIR}/include) | |
endif() |
elseif
if (EXISTS ${CMAKE_SOURCE_DIR}/include) | |
include_directories(${CMAKE_SOURCE_DIR}/include) |
foreach 命令用于循环
foreach(<loop_var> <items>) | |
<commands> | |
endforeach() |
<loop_var> :表示循环变量
# 日志
message 命令用于输出日志信息,其语法如下:
message(<mode> "message to display" ...) |
<mode> :表示输出信息的模式,可以是 (空) 、 STATUS 、 WARNING 、 AUTHOR_WARNING 、 SEND_ERROR 、 FATAL_ERROR 、 DEPRECATION 之一
"message to display" :表示要输出的信息
| 模式 | 输出信息 |
|---|---|
| (空) | 输出重要信息 |
| STATUS | 输出普通信息 |
| WARNING | 输出警告信息 |
| AUTHOR_WARNING | 输出作者警告信息 |
| SEND_ERROR | 输出错误信息,继续执行 |
| FATAL_ERROR | 输出错误信息,终止执行 |
| DEPRECATION | 输出弃用信息 |
CMake 命令行工具会在 stdout 上显示 STATUS 消息,在 stderr 上显示其他消息。CMake GUI 工具会在消息窗口中显示所有消息。
# 变量操作
有时候项目源文件比较多并且不再同一目录下,这些源文件需要一起编译生成可执行文件,我们可以通过 file 命令对各自目录下的源文件进行搜索,最后还需要做一个字符串拼接操作,关于字符串拼接可以使用 set 命令也可以使用 list 命令。
set 命令
set 命令用于设置变量的值,其语法如下:
# 取出 ${} 中变量的字符串 ---> 拼接字符串 ---> 赋值给变量 | |
set(<变量名> ${变量名1} ${变量名2}... ) | |
# 将 string1 string2... 的字符串拼接 ---> 赋值给变量 | |
set(<变量名> string1 string2... ) |
set 实际是字符串拼接,该命令是将 变量名1 、 变量名2 … 的值进行拼接并赋值给 变量名 ,如果 变量名 已经存在,则覆盖其值,如果 变量名 不存在,则创建该变量。
list 命令
字符串拼接
list(APPEND <list> [args...]) |
APPEND :将 args 字符串依次添加到 list 的末尾, APPEND 之后写法与 set 一致,与 set 命令不同的是, list 命令不会覆盖 list 的值,而是将 args 字符串依次添加到 list 的末尾。
字符串移除
当通过 file 查找某个目录下的所有源文件时,其中有些使我们不需要的,例如下面的 main.cpp
$ tree | |
. | |
├── add.cpp | |
├── div.cpp | |
├── main.cpp | |
├── mult.cpp | |
└── sub.cpp |
其中 main 是测试文件,如果我们想把计算器相关的源文件生成动态库给别人使用,只需要 add.cpp 、 div.cpp 、 mult.cpp 、 sub.cpp 这四个文件,那么我们可以通过 list 命令将 main.cpp 移除。
list(REMOVE_ITEM <list> [value...]) |
REMOVE_ITEM :将 list 中所有 value 字符串移除。
字符串查找
list(FIND <list> <value> <variable>) |
FIND :在 list 中查找 value 字符串,如果找到,则将 value 字符串在 list 中的索引赋值给 variable ,如果未找到,则将 -1 赋值给 variable 。
字符串替换
list(REPLACE <list> <value> <newvalue>) |
REPLACE :将 list 中所有 value 字符串替换为 newvalue 字符串。
| 命令 | 描述 | 使用示例 |
|---|---|---|
| APPEND | 将 args 添加到列表的末尾 | list(APPEND <list> [args...]) |
| APPEND_STRING | 将 args 添加到列表的末尾 | list(APPEND_STRING <list> [args...]) |
| APPEND_UNIQUE | 将 args 添加到列表的末尾,如果 args 已经存在,则不添加 | list(APPEND_UNIQUE <list> [args...]) |
| CLEAR | 清除列表 | list(CLEAR <list>) |
| GET | 获取列表中的元素 | list(GET <list> [<index>...] [<output variable>...]) |
| INSERT | 在 列表 中的 某一位置 插入 (索引前插入) 元素 得到 新字符串 |
list(INSERT <list> <index> [<index>...] [<output variable>...]) |
| LENGTH | 获取列表的长度并赋值给 变量 |
list(LENGTH <list> <variable>) |
| POP_BACK | 删除 列表 中的最后一个元素获取 新字符串 |
list(POP_BACK <list> [<output variable>...]) |
| POP_FRONT | 删除 列表 中的第一个元素获取 新字符串 |
list(POP_FRONT <list> [<output variable>...]) |
| PREPEND | 将 args 添加到列表的开头 | list(PREPEND <list> [args...]) |
| REMOVE_AT | 删除 列表 中的 某一位置 的元素获取 新字符串 |
list(REMOVE_AT <list> [<index>...] [<output variable>...]) |
| REMOVE_ITEM | 删除 列表 中所有 value 字符串 |
list(REMOVE_ITEM <list> [value...]) |
| REMOVE_DUPLICATES | 删除 列表 中重复的元素 |
list(REMOVE_DUPLICATES <list>) |
| JOIN | 将 列表 中的元素用 连接符 连接成一个 新字符串 |
list(JOIN <list> <glue> <output variable>) |
| REVERSE | 将 列表 中的元素反转 |
list(REVERSE <list>) |
| SORT | 将 列表 中的元素排序 |
list(SORT <list>) |
# 宏定义
在程序测试中,我们经常需要定义一些宏,例如 DEBUG ,通过这些宏来控制程序是否打印调试信息
#include <iostream> | |
#define DEBUG | |
int main(){ | |
int n =10; | |
#ifdef DEBUG | |
std::cout << "debug n=" << n << std::endl; | |
#endif | |
return 0; | |
} |
程序通过 #ifdef DEBUG 判断 DEBUG 是否被定义,如果被定义,则打印调试信息。如果没有被定义,则不打印调试信息,该端代码相当于被注释掉了,因此无法看到日志输出。
为了使得 c++ 测试更加方便,我们可以不在代码中定义宏,在 g++/gcc 命令中定义宏,例如
g++ -DDEBUG main.cpp -o main |
g++/gcc 命令通过 -D 指定定义宏的名字,这样在代码中就可以使用 DEBUG 宏了。
CMake 中使用 add_definitions 定义宏
add_definitions(-DDEBUG) |
add_definitions 命令用于定义宏,其中 -D 是 g++/gcc 命令的参数, DEBUG 是宏的名字。
# 嵌套 CMake
在 Linux 的目录是树状结构,所以嵌套 CMake 也是树状结构,最顶层的 CMakeLists.txt 文件称为 根节点 ,其他 CMakeLists.txt 文件称为 子节点 。需要了解关于 CMakeLists.txt 的几个信息:
- 根节点的
CMakeLists.txt中定义变量全局有效。 - 父节点的
CMakeLists.txt中定义的变量,子节点可以访问。 - 子节点的
CMakeLists.txt中定义的变量,父节点无法访问。
添加子目录
CMake 中父子节点是如何建立的?
add_subdirectory(source_dir [binary_dir] [EXCLUDE_FROM_ALL]) |
source_dir : 子目录的路径,
binary_dir : 指定输出文件的路径,一般不需要指定,默认即可。
EXCLUDE_FROM_ALL : 在子路径下的目标默认不会被包含到父路径的 ALL 目标中,并且也会排除在 IDE 工程文件之外。用户必须显式构建在子路径下的目标。
获取父目录的路径
get_filename_component(PARENT_DIR ${CMAKE_CURRENT_SOURCE_DIR} PATH) |
get_filename_component 命令用于获取文件路径,其中 PARENT_DIR 是变量名, ${CMAKE_CURRENT_SOURCE_DIR} 是当前目录的路径, PATH 表示获取路径。