编译环境:在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表示获取路径。