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