一、准备知识
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不会链接BINTERFACE:依赖项B并不链接到目标A,如果有C链接了A,C会链接BPUBLIC:依赖项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__