0 . CMake入门--程序构建和项目结构
参考:
https://subingwen.cn/CMake/CMake-primer/?highlight=CMake
《modern Cmake for C++》
《Cmake构建实战-项目开发卷》
1 程序构建和 Makefile
1.1 程序构建
程序构建的流程
编译器是一套工具链, 分为4部分:
- 预处理器
- 输入: 代码源文件
- 输出: 处理后的源文件
- 功能: 头文件展开, 宏替换, 去除注释等
- 编译器
- 如常见的 gcc
- 输入: 处理后的源文件
- 输出: 汇编文件
- 功能: 将源文件编译为汇编文件
- 汇编器
- 输入: 汇编文件
- 输出: 二进制文件, 如
.obj,.o - 功能: 将源文件编译为汇编文件
- 链接器
- 输入: 二进制的
.obj等 - 输出: 打包后的可执行程序, 二进制的
- 功能: 把这些⽬标⽂件链接在⼀起, 解析其中未定义的符号引⽤
- 输入: 二进制的
例子
g++ -c main.cpp head.cpp
-c 表示编译, 将生成两个独立的 obj 文件
g++ main.o slow.o
这是链接过程, gcc 程序实际上调用了链接器 ld.exe (这里是 windows 系统下的后缀名), 最终生成可执行文件
按需编译
在上面例子中
main.o slow.o是两个独立的编译后文件- 当一个源文件, 比如
main.cpp改变时, 只需要编译main.cpp即可 - 然后再重新链接
步骤
g++ -c main.cpp
g++ main.o slow.o
1.2 make 程序和 Makefile
Makefile 的作用
当源文件数量少的时候, 直接在 终端 中输入编译命令即可
但当文件数量非常多, 直接用命令则需要特别长的命令, 此时就需要 Makefile 脚本的帮助.
- Makefile 文件的文件名就是
makefile或Makefile - Makefile 是脚本, 写了一些指令
- 在终端中使用命令
make, 它就会用 make 程序 执行 Makefile 脚本. 从而实现编译
不同的编译环境,这个make.exe的名称可能不同
在windows下,用msys2安装的clang64环境下的make工具,它的全称为`mingw32-make.exe`
Makefile 例子
main: main.o slow.o
g++ -o main main.o slow.o
main.o: main.cpp
g++ -c main.cpp -o main.o
slow.o: slow.cpp
g++ -c slow.cpp -o slow.o
这里有三条指令
- 冒号后面, 是构建的源文件 比如
main.o slow.o - 冒号前面, 是目标 比如
main - 缩进的行是对应的指令
make 程序 实现按需编译
- 在 Makefile 文件中编写 编译和链接的规则
- 使用 make 程序将自动执行
Makefile中的规则 - 它能自动按需编译
make如何实现按需编译?
- 它会检查源文件的**更新时间**, 以及已存在的目标的更新时间
- 如果源文件 比 目标文件的构建时间 更晚, 那么就会执行那条命令
2 项目的结构
项目文件
项目一般至少含有两类文件:
- 源代码 (src): 包含了项目的核心逻辑,用编程语言编写,如 C++、Python、Java 等。
- 编译生成文件 (build): 由编译器生成的中间文件或最终的可执行文件、库文件等。
更多的情况:
| 文件类型 | 作用描述 | 示例 |
|---|---|---|
| 库文件 (lib) | 提供可重用的代码和功能,分为静态库和动态库 | C 标准库(如stdio库等),自定义的静态库mylib.a、动态库mylib.so等 |
| 资源文件 (res) | 包含项目运行所需的其他资源,如图片、音频、视频、字体、配置文件、数据文件等 | 游戏中的角色图片、背景音乐文件,软件的配置ini文件、存储用户信息的数据文件等 |
| 配置文件 (config) | 用于配置项目的各种设置,包括编译选项、运行参数、环境变量等 | CMakeLists.txt, 环境参数, IDE 配置, git 配置 |
| 项目文档 (doc) | 描述项目结构、功能、使用方法等 | README.md 文件,Java 项目生成的 API 文档等 |
| 测试文件 (test) | 包含单元测试、集成测试等,用于验证代码的正确性 | Python 项目中使用unittest框架编写的测试文件,Java 项目中基于JUnit的测试代码文件等 |
示例项目结构:
my_project/
├── src/
│ ├── main.cpp
│ └── utils.h
├── build/
│ ├── CMakeFiles
│ └── my_project
├── lib/
│ ├── my_lib.a
│ └── third_party_lib.so
├── res/
│ ├── images/
│ ├── sounds/
│ └── data/
├── config/
│ ├── CMakeLists.txt
│ └── app.config
├── doc/
│ ├── README.md
│ └── design.pdf
└── test/
├── unit_tests.cpp
└── integration_tests.py
库文件, 源文件, 头文件
- 源文件(.c/.cpp):
- 包含了程序的源代码,是程序的实现部分。
- 其中定义了函数、变量、类等。
- 经过编译后生成目标文件(.o/.obj)。
- 头文件(.h):
- 包含函数、变量、类的声明,但不包含具体的实现。
- 用于提供接口,让其他文件可以引用这些声明。
- 通常被多个源文件包含。
- 库文件(.lib/.a/.so/.dll):
- 是由编译好的目标文件链接而成的。
- 提供可重用的代码和功能。
- 可以是静态库(.lib/.a)或动态库(.so/.dll)。
- **头文件和源文件的关系:**
- 头文件中的声明对应源文件中的实现。
- 源文件通常会包含对应的头文件,以便使用头文件中的声明。
- 比如a.cpp 和 a.h
- **源文件和库文件的关系:**
- 多个源文件编译后链接在一起,形成库文件。
- 库文件中的函数可以在其他程序中被调用。
- **头文件和库文件的关系:**
- *头文件提供了库的接口,告诉用户如何使用库中的函数*。
- 库文件提供了函数的具体实现。
库文件比源文件的优势
- 一些"库文件"不是以
lib或so等形式的, 而是直接就是代码, 比如 C++的STL 库 - 编译可以生成库文件 或 可执行程序
- 相比于直接给出源代码, 在网络上分发 库文件有一些好处
- 保密性: 隐藏了原始代码, 防止剽窃
- 便于管理和使用: 如果源文件数量多, 则不便管理, 而将它们编译为一个
lib则更方便分发和管理
静态库和动态库的概念
静态库 (Static Library)
- 链接方式: 在编译时,静态库的代码会被复制到使用它的程序中,成为程序的一部分。
- 扩展名:
.lib(Windows) 或.a(Linux/Unix) - 优点:
- 程序运行时不需要依赖外部库文件,因为代码已经包含在程序内部。
- 程序运行速度更快,因为不需要在运行时进行链接操作。
- 缺点:
- 编译后的程序体积较大,因为包含了库代码的副本。
- 如果库更新,需要重新编译所有使用该库的程序。
动态链接库
- 链接方式: 在编译时,动态库的代码不会被复制到程序中,而是在程序运行时才被加载。
- 扩展名:
.dll(Windows) 或.so(Linux/Unix) - 优点:
- 编译后的程序体积较小,因为不包含库代码。
- 多个程序可以共享同一个动态库文件,节省内存和磁盘空间。
- 动态库可以独立更新,而不需要重新编译使用它的程序。
- 缺点:
- 程序运行时需要依赖外部库文件,如果找不到库文件,程序将无法运行。
- 程序运行速度稍慢,因为需要在运行时进行链接操作。
动态链接的原理
- 启动进程时,Windows 操作系统会*装载*进程所需的动态链接库, 并调⽤动态链接库的*⼊⼝函数*
- Windows 操作系统默认启⽤*地址空间布局随机化*, 因此装载所在的内存位置是随机的
如何使用静态库
- 确保项目树中有对应的头文件, 在源码中使用
include包含, 然后使用它的 API - 编译时,通过
-L选项指定静态库文件所在的文件夹路径 (注意是lib文件的路径, 而不是库文件对应的头文件的路径) - 链接时, 使用
-l选项指定要链接的库文件名(去掉前缀lib和文件后缀)
例子
假设我们有一个库文件 libmymath.so,它位于 /usr/local/lib 目录下。要链接这个库,我们可以使用以下命令:
g++ main.cpp -L/usr/local/lib -lmymath
-L/usr/local/lib:告诉编译器去/usr/local/lib目录下寻找库文件。-lmymath:告诉编译器要链接名为mymath的库。
为什么编译时 `-L` 指定了 .lib路径, 链接时还要 `-l` 指定链接的具体文件名
- `-L` 指定搜索路径,扩大编译器寻找库文件的范围
- `-l` 指定库名, 在找到库文件夹后,需要根据库名来确定要链接哪个库
如何使用动态库
- 准备动态库文件和对应的头文件, 并在代码中使用
#include - 链接动态库, 有两种方法
- 隐式链接: 和使用静态库类似
- 在编译链接时,使用
-l选项指定动态库名,使用-L选项指定动态库的搜索路径。 - 编译器会在运行时自动加载动态库。
- 运行隐式链接的程序必须保证: 程序运行时可以找到库文件 (需要加入环境变量 path 或者放在同一目录下)
- 在编译链接时,使用
- 显式链接:
- 在程序运行时,使用系统提供的 API 函数 动态加载动态库
- 如 Windows 的
LoadLibrary打开一个.dll文件 - Linux 系统下
dlopen打开.so文件
- 如 Windows 的
- 这种方式更灵活,但实现起来也相对复杂。
- 在程序运行时,使用系统提供的 API 函数 动态加载动态库
- 隐式链接: 和使用静态库类似
动态库的隐式链接和 使用静态库很相似但:
- 在链接静态库时,链接器会将静态库中的代码和数据*复制到最终的可执行文件*中
- 在隐式链接动态库时,编译器只是在可执行文件中记录了需要加载动态库的信息(如库的名称、路径等),并没有将库中的代码复制到可执行文件中
显式链接的例子
Linux 系统下 使用 dlopen 函数打开动态库,获取一个库句柄
再通过 dlsym 函数从库句柄中获取要使用的函数指针
例子中假设 动态库中有一个 add 函数,函数原型为 int add(int, int)
void *handle = dlopen("./libs/libmydynamic.so", RTLD_LAZY);
if (!handle) {
// 处理打开失败的情况
}
else {
int (*add)(int, int) = (int(*)(int, int))dlsym(handle, "add");
if (add == NULL) {
// 处理获取函数指针失败的情况
}
}

浙公网安备 33010602011771号