编译环境:在 Wiondows 下,使用 VScode+WSL2+Ubuntu20.04+GCC 编译器


# 核心命令

# 项目配置

  1. cmake_minimum_required 指定 CMake 的最小版本要求
cmake_minimum_required(VERSION 3.10)

project 定义项目名称,并可以指定项目使用的编程语言、web 主页地址、项目版本号等

project(MyProject VERSION 1.0 LANGUAGES CXX)
  1. set 设置变量
#方式 1 各源文件剪枝空格间隔,并赋值给 SOURCE(变量名任取,不要重复即可)变量
set(SOURCES main.cpp foo.cpp bar.cpp)
#方式 2 各源文件 “;” 间隔
set(SOURCES main.cpp;foo.cpp;bar.cpp)
# 使用变量,采用 ${} 方式
add_executable(MyExecutable ${SOURCES})
  1. 指定使用 c++ 标准

命令行编译时使用 c++ 标准,通过 -std=c++11 格式指定

g++  -std=c++11 -o demo main.cpp
  1. 通过 set 命令指定

CMake 文件指定 c++ 标准

set(CMAKE_CXX_STANDARD 11)
  1. 通过 project 命令指定
project(MyProject VERSION 1.0 LANGUAGES CXX CXX_STANDARD 11)
  1. 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++ 标准

# 文件管理

  1. 指定输出路径

在 CMake 中指定可执行程序输出的路径,也有对应的宏,叫做 EXECUTABLE_OUTPUT_PATH ,可通过 set 命令指定,例如:

set(HOME /home/zhang/Linux)
set(EXECUTABLE_OUTPUT_PATH ${HOME}/bin)
  • 第一行:定义一个变量存储一个路径
  • 第二行:将拼接好的路径赋值给 EXECUTABLE_OUTPUT_PATH 变量
  1. 搜索文件

aux_source_group 查找 某路径 下的 所有源文件

aux_source_group("Source Files" variable)
  • Source Files :表示搜索的文件所在的目录名称
  • variable :将 Source Files 目录下搜索到的源文件列表赋值给 variable 变量
  1. 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 变量。
  1. file 查找文件
    使用 file 命令,查找某路径下的 某格式文件
file(GLOB/GLOB_RECURSE 变量名 要搜索文件路径和文件类型)
  • GLOB :表示搜索指定目录下所有文件,将满足条件的文件列表赋值给变量
  • GLOB_RECURSE :表示递归搜索指定目录下所有文件,将满足条件的文件列表赋值给变量
  1. 包含头文件
    在编译项目源文件时,需要将源文件对应的头文件包含进来,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 :表示链接库的作用域,可以是 PRIVATEPUBLICINTERFACE 之一

# 工程实践

# 标准结构

# 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_executableadd_library 命令之后)

target_link_libraries(<target> [PRIVATE|PUBLIC|INTERFACE] <item>...)

<target> :表示目标可执行文件或库的名称
[PRIVATE|PUBLIC|INTERFACE] :表示链接库的访问权限,默认为 PUBLIC , 可以是 PRIVATEPUBLICINTERFACE 之一
<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> :表示输出信息的模式,可以是 (空)STATUSWARNINGAUTHOR_WARNINGSEND_ERRORFATAL_ERRORDEPRECATION 之一
"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.cppdiv.cppmult.cppsub.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 命令用于定义宏,其中 -Dg++/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 表示获取路径。