
一、准备知识
1.1 C++的编译过程
使用g++
等编译工具,从源码生成最终的可执行文件一般有这几步:预处理(Preprocess)、编译(Compile)、汇编(assemble)、链接(link)。

输入
g++ --help
可以看到对应命令:
1
2
3
4 -E Preprocess only; do not compile, assemble or link.
-S Compile only; do not assemble or link.
-c Compile and assemble, but do not link.
-o <file> Place the output into <file>.
以下面程序为例:
1 |
|
第一步:预处理
C++中预处理指令以#
开头。在预处理阶段,会对#define
进行宏展开,处理#if,#else
等条件编译指令,递归处理#include
。这一步需要我们添加所有头文件的引用路径。1
2# 将xx.cpp源文件预处理成xx.i文件(文本文件)
g++ -E main.cpp -o main.i第二步:编译
检查代码的规范性和语法错误等,检查完毕后把代码翻译成汇编语言文件。
1
2# 将xx.i文件编译为xx.s的汇编文件(文本文件)
g++ -S main.i -o main.s第三步:汇编
基于汇编语言文件生成二进制格式的目标文件。1
2# 将xx.s文件汇编成xx.o的二进制目标文件
g++ -c main.s -o main.o第四步:链接
将目标代码与所依赖的库文件进行关联或者组装,合成一个可执行文件
1
2# 将xx.o二进制文件进行链接,最终生成可执行程序
g++ main.o -o main
1.2 静态链接库和动态链接库
所谓静态和动态,其区别是链接的阶段不一样。
静态链接库名称一般是
lib库名称.a
(.a
代表archive library
),其链接发生在编译环节。一个工程如果依赖一个静态链接库,其输出的库或可执行文件会将静态链接库*.a
打包到该工程的输出文件中(可执行文件或库),因此生成的文件比较大,但在运行时也就不再需要库文件了。而动态链接库的链接发生在程序的执行过程中,其在编译环节仅执行链接检查,而不进行真正的链接,这样可以节省系统的开销。动态库一般后缀名为
*.so
(.so
代表shared object
,Linux:lib库名称.so
,macOS:lib库名称.dylib
)。动态链接库加载后,在内存中仅保存一份拷贝,多个程序依赖它时,不会重复加载和拷贝,这样也节省了内存的空间。以下图为例
工程
A
和B
依赖静态链接库static library
,A
和B
在运行时,内存中会有多份static library
;工程
A
和B
依赖动态链接库shared library
,A
和B
在运行时,内存中只有一份shared library
(shared:共享)。
以上只是非常简单的一个解释以区分动态链接库和静态链接库。更多底层的知识需要单独进行深入讲解。
1.3 为什么需要CMake
1.3.1 g++ 命令行编译
当我们编译附件中1.hello_world
时,我们可以运行
1 | g++ main.cpp -o main |
当我们需要引入外部库时,如附件中的2.external_libs
,需要引入gflags
(Google开源的命令行参数处理库),我们则需要运行:
1 | # 安装gflags |
有些时候有一些常用库我们也不用手动添加头文件或链接库路径,通常g++能在默认查询路径中找到他们。当我们的项目文件变得多起来,引入的外部库也多起来时,命令行编译这种方式就会变得十分臃肿,也不方便调试和编辑。通常在测试单个文件时会使用命令行进行编译,但不推荐在一个实际项目中使用命令行编译。
1.3.2 CMake简介
在实际工作中推荐使用CMake构建C++项目,CMake是用于构建、测试和软件打包的开源跨平台工具;
特性:
- 自动搜索可能需要的程序、库和头文件的能力;
- 独立的构建目录(如
build
),可以安全清理 - 支持复杂的自定义命令(下载、生成各种文件)
- 自定义配置可选组件
- 从简单的文本文件(
CMakeLists.txt
)自动生成工作区和项目的能力 - 在主流平台上自动生成文件依赖项并支持并行构建
- 几乎支持所有的IDE
二、CMake基础知识
2.1 安装
ubuntu上请执行
1 | sudo apt install cmake -y |
或者编译安装:
1 | 以v3.25.1版本为例 |
2.2 第一个CMake例子
附件位置:3.first_cmake
1 | # 第一步:配置,-S 指定源码目录,-B 指定构建目录 |
vs code插件:
- 安装
twxs.cmake
做代码提示; - 安装
ms-vscode.cmake-tools
界面操作。
2.3 语法基础
2.3.1 指定版本
以附件:3.first_cmake/CMakeLists.txt
为例:
1 | # CMake 最低版本号要求 |
命令cmake_minimum_required
来指定当前工程所使用的CMake版本,不区分大小写的,通常用小写。VERSION
是这个函数的一个特殊关键字,版本的值在关键字之后。CMake中的命令大多和cmake_minimum_required
相似,不区分大小写,并有很多关键字来引导命令的参数输入(类似函数传参)。
2.3.2 设置项目
以附件:3.first_cmake/CMakeLists.txt
为例:
1 | project(ProjectName |
在CMakeLists.txt
的开头,都会使用project
来指定本项目的名称、版本、介绍、与使用的语言。在project
中,第一个ProjectName
(例子中用的是first_cmake
)不需要参数,其他关键字都有参数。
2.3.3 添加可执行文件目标
以附件:3.first_cmake/CMakeLists.txt
为例:
1 | add_executable(first_cmake main.cpp) |
这里我们用到add_executable
,其中第一个参数是最终生成的可执行文件名以及在CMake中定义的Target
名。我们可以在CMake中继续使用Target
的名字为Target
的编译设置新的属性和行为。命令中第一个参数后面的参数都是编译目标所使用到的源文件。
2.3.4 生成静态库并链接
附件位置:4.static_lib_test
A.生成静态库
1 | #account_dir/CMakeLists.txt |
1 | 编译静态库后,会在build下生成 build/libAccount.a 静态库文件 |
这里我们用到add_library
, 和add_executable
一样,Account
为最终生成的库文件名(lib库名称.a
),第二个参数是用于指定链接库为动态链接库(SHARED
)还是静态链接库(STATIC
),后面的参数是需要用到的源文件。
B.链接
1 | # test_account/CMakeLists.txt |
1 | 编译后目录如下 |
我们通过add_library
和add_executable
定义了Target
,我们可以通过Target
的名称为其添加属性,例如:
1 | # 指定目标包含的头文件目录 |
- 通过
target_include_directories
,我们给test_account
添加了头文件引用路径"../account_dir"
。上面的关键词PUBLIC
,PRIVATE
用于说明目标属性的作用范围,更多介绍参考下节。 - 通过
target_link_libraries
,将前面生成的静态库libAccount.a
链接给对象test_account
,但此时还没指定库文件的目录,CMake无法定位库文件 - 再通过
target_link_directories
,添加库文件的目录即可。
2.3.5 生成动态库并连接
附件位置:5.dynamic_lib_test
A.生成动态库
1 | #account_dir/CMakeLists.txt |
1 | 编译动态库后,会在build下生成 build/libAccount.so 动态库文件 |
B.链接
操作不变。
1 | ldd查看依赖的动态库 |
当然,也可以用一个CMakeLists.txt
来一次性编译,参考附件6.build_together
1 | #6.build_together/CMakeLists.txt` |
2.3.6 CMake 中的 PUBLIC、PRIVATE、INTERFACE
CMake中经常使用target_...()
类似的命令,一般这样的命令支持通过PUBLIC
、PRIVATE
、INTERFACE
关键字来控制传播。
以target_link_libraries(A B)
为例,从理解的角度来看
PRIVATE
:依赖项B
仅链接到目标A
,如果有C
链接了A
,C
不会链接B
INTERFACE
:依赖项B
并不链接到目标A
,如果有C
链接了A
,C
会链接B
PUBLIC
:依赖项B
链接到目标A
,如果有C
链接了A
,C
也会链接B
其实就是对象属性的传递,打个散烟的比方:
PRIVATE
: 就是自己抽,不给别人抽INTERFACE
:就是自己不抽,给别人抽PUBLIC
:就是自己抽,也给别人抽
从使用的角度来说,如果有C
链接了目标A
- 如果
B
仅用于A
的实现,且不在头文件中提供给C
使用,使用PRIVATE
- 如果
B
不用于A
的实现,仅在头文件中作为借口给C
使用,使用INTERFACE
- 如果
B
既用于A
的实现,也在头文件中提供给C
使用,使用PUBLIC
举例:
1 | # 创建库 |
- 因为
C
是B
的PUBLIC
依赖项,所以C
会传播到A
- 因为
D
是B
的PRIVATE
依赖性,所以D
不会传播到A
2.3.7 变量
附件位置:7.message_var_demo
像其他编程语言一样,我们应该将CMake理解为一门编程语言。我们也需要设定变量来储存我们的选项,信息。有时候我们通过变量来判断我们在什么平台上,通过变量来判断我们需要编译哪些Target
,也通过变量来决定添加哪些依赖。
2.3.8 include引入其他代码
附件位置:8.include_demo
2.3.9 条件控制
附件位置:9.if_demo
正如前面所讲,应该把CMake当成编程语言,除了可以设置变量以外,CMake还可以写条件控制。
1 | if(variable) |
可以和条件一起使用的关键词有
1 | NOT, TARGET, EXISTS (file), DEFINED等 |
2.3.10 CMake分步编译
附件位置:10.steps_demo
1 | # 查看所有目标 |
2.3.11 生成器表达式
附件位置:11.generator_expression
生成器表达式简单来说就是在CMake生成构建系统的时候根据不同配置动态生成特定的内容。有时用它可以让代码更加精简,我们介绍几种常用的。
需要注意的是,生成表达式被展开是在生成构建系统的时候,所以不能通过解析配置
CMakeLists.txt
阶段的message
命令打印,可以用类似file(GENERATE OUTPUT "./generator_test.txt" CONTENT "$<$<BOOL:TRUE>:TEST>")
生成文件的方式间接测试。
在其最一般的形式中,生成器表达式是$<...>
,尖括号中间可以是如下几种类型:
- 条件表达式
- 变量查询(Variable-Query)
- 目标查询(Target-Query)
- 输出相关的表达式
1 | # 1.条件表达式:$<condition:true_string>,当condition为真时,返回true_string,否则返回空字符串 |
4.输出相关表达式:用于在不同的环节使用不同参数,比如需要在install
和build
环节分别用不同的参数,我们可以这样写:
1 | add_library(Foo ...) |
其中$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>
仅在build
环节生效;而$<INSTALL_INTERFACE:${CMAKE_INSTALL_INCLUDEDIR}>
仅在install
环节生效。通过设定不同阶段不同的参数,我们可以避免路径混乱的问题。
2.3.12 函数和宏
附件位置:12.function_macro
1 | # 定义一个宏,宏名为my_macro,没有参数 |
2.3.13 设置安装
附件位置:13.install_demo
当需要发布项目时你需要指定项目文件的安装路径。下面的代码片段中,使用install
安装demo_test
,并分别将可执行文件安装在bin
中,动态链接库和静态链接库都安装在lib
,公共头文件安装在include
。这里的路径都将添加${CMAKE_INSTALL_PREFIX}
作为前缀(如果不设置CMAKE_INSTALL_PREFIX
,则会安装到/usr/local
目录下)。实现安装的功能在你需要发布你项目给其他人使用时,非常有用。
1 | # 设置安装 |
2.3.14 寻找依赖 find_package
对于大部分支持了CMake的项目来说,均可以通过find_package
找到对应的依赖库,参考附件:14.find_demo
1 | 使用find_package寻找<LibaryName>库,如果找到,一般都会有以下变量(库作者设置) |
假设我们编写了一个新的函数库,我们希望别的项目可以通过find_package
对它进行引用,我们有两种办法:
- 编写一个
Find<LibraryName>.cmake
,适用于导入非cmake安装的项目,参考附件:15.custom_find
- 使用
install
安装,生成<LibraryName>Config.cmake
文件,适用于导入自己开发的cmake项目,参考附件:16.custom_install_demo
三、opencv CMake示例
附件位置:17.demo_opencv/
安装OpenCV:sudo apt install libopencv-dev
依赖和链接OpenCV与常规的添加依赖并没有太多不同,同时OpenCV提供了cmake find package
的功能,因此我们可以通过find_package
方便的定位opencv在系统中的位置和需要添加的依赖。
1 | find_package(OpenCV REQUIRED) |
如果cmake找到了OpenCV,配置cmake后,命令行会有如下输出:
1 | OPENCV INCLUDE DIRS: /usr/include/opencv4 |
__END__