C++ 静态库与动态库详解及 CMake 构建指南
一、引言
在软件开发中,**库(Library)**是指一组预先编写好的、可复用的代码模块,旨在简化开发过程、提高代码复用性和维护性。库本质上是一种可执行代码的二进制形式,可以被操作系统载入内存执行。根据链接方式的不同,库主要分为**静态库(Static Library)**和**动态库(Dynamic Library)**。
二、库的基本概念
1. 静态库(Static Library)
- 文件后缀:
- Windows 下:
.lib - Unix/Linux 下:
.a
- Windows 下:
- 特点:
- 编译时链接:在编译过程中,静态库的代码会被复制并链接到最终的可执行文件中。
- 独立性:生成的可执行文件不依赖于外部库文件,运行时无需额外的库支持。
- 体积较大:因为库代码被嵌入到每个使用它的可执行文件中,导致可执行文件体积增加。
- 版本一致性:不同项目使用的静态库版本是固定的,更新库后需重新编译依赖的可执行文件以应用新版本。
2. 动态库(Dynamic Library)
- 文件后缀:
- Windows 下:
.dll - Unix/Linux 下:
.so
- Windows 下:
- 特点:
- 运行时链接:动态库在程序运行时加载,库代码不被嵌入到可执行文件中。
- 共享性:多个可执行文件可以共享同一个动态库,节省内存和存储空间。
- 灵活性:更新动态库后,所有依赖该库的程序无需重新编译即可使用新版本。
- 依赖性:运行时需要确保动态库文件存在,否则程序无法启动。
三、静态库与动态库的对比
| 特性 | 静态库(.lib/.a) | 动态库(.dll/.so) |
|---|---|---|
| 链接时间 | 编译时 | 运行时 |
| 文件大小 | 可执行文件较大 | 可执行文件较小,库单独存放 |
| 更新与维护 | 更新库需要重新编译所有依赖项 | 更新库无需重新编译,多个程序可共享库 |
| 内存使用 | 每个程序独立加载库代码 | 多个程序共享同一库的内存空间 |
| 平台依赖性 | 较少(生成的可执行文件独立) | 需要确保运行环境中存在相应的动态库 |
| 性能 | 链接时已完成,可能略快于动态库 | 运行时需加载库,初次加载可能稍慢 |
四、C++ 库的创建与使用
1. 创建库的源代码示例
以一个简单的数学库 mymath 为例,提供计算整数和浮点数之和的功能。
a. 头文件 mymath.h
1 |
|
解释
:
MYMATH_API宏用于控制符号的导出与导入。在构建动态库时定义MYMATH_EXPORTS以导出符号;在使用库时不定义该宏,以导入符号。extern "C"避免 C++ 的名字修饰(Name Mangling),确保库函数具有 C 风格的接口,便于跨语言调用。
b. 源文件 mymath.cpp
1 |
|
2. 使用 CMake 构建库
CMake 是一个跨平台的自动化构建系统,能够简化构建过程。以下介绍如何使用 CMake 构建静态库和动态库。
a. 项目结构
假设项目结构如下:
1 | MyMathLib/ |
b. 创建静态库的 CMakeLists.txt
1 | cmake_minimum_required(VERSION 3.0) |
c. 创建动态库的 CMakeLists.txt
1 | cmake_minimum_required(VERSION 3.0) |
d. 同时构建静态库和动态库的 CMakeLists.txt
如果需要同时生成静态库和动态库,可以在同一个 CMakeLists.txt 中添加多个库目标:
1 | cmake_minimum_required(VERSION 3.0) |
3. 编译库
在项目根目录下执行以下命令以编译库:
1 | mkdir build |
根据 CMakeLists.txt 的配置,这将在 build 目录下生成静态库和/或动态库文件。
五、在项目中使用库
1. 示例项目结构
假设有一个应用程序项目 MyApp,其结构如下:
1 | MyApp/ |
2. 编写应用程序源代码 main.cpp
1 |
|
3. 编写应用程序的 CMakeLists.txt
1 | cmake_minimum_required(VERSION 3.0) |
注意:
- 对于 Windows 平台,动态库的链接和部署可能需要额外配置,确保
.dll文件在可执行文件的同一目录或系统路径下。 - 使用相对路径链接库时,应确保路径的正确性,或考虑使用 CMake 的
find_library等功能进行更灵活的库查找。
4. 构建并运行应用程序
在 MyApp 项目根目录下执行:
1 | mkdir build |
运行后,应输出:
1 | Sum (int): 7 |
六、注意事项
- 宏定义与符号导出:
- 在动态库的头文件中,通过宏定义控制符号的导出和导入。在 Windows 平台,使用
__declspec(dllexport)导出符号,__declspec(dllimport)导入符号;而在 Unix/Linux 平台,通常不需要特殊标记。
- 在动态库的头文件中,通过宏定义控制符号的导出和导入。在 Windows 平台,使用
- 跨平台兼容性:
- 不同平台的动态库文件后缀和符号导出方式不同。CMake 可以根据平台条件设置宏定义和库属性,确保库在各个平台上的正确构建和使用。
- 安装与部署:
- 使用
install指令指定库文件和头文件的安装路径,使得其他项目可以方便地引用和链接这些库。 - 动态库需要在运行时能够被找到,可以通过环境变量
PATH(Windows)或LD_LIBRARY_PATH(Unix/Linux)设置库的查找路径,或将库文件放在系统默认路径下。
- 使用
- 依赖管理:
- 对于较大的项目,可以使用 CMake 的
find_package或add_subdirectory来管理多个库和依赖,保持项目结构清晰。
- 对于较大的项目,可以使用 CMake 的
- 版本控制:
- 设置库的版本号,有助于管理不同版本的库及其兼容性。例如,使用
VERSION和SOVERSION属性控制动态库的主版本和次版本,便于动态链接器进行版本匹配。
- 设置库的版本号,有助于管理不同版本的库及其兼容性。例如,使用
封装任务管理系统
日志库设计
Logger.h声明不变
1 |
|
Logger.cpp具体实现也不变
1 |
|
Logger库编写
1 | cmake_minimum_required(VERSION 3.28) |
纯虚类设计
我们之前采用CRTP方式封装了任务管理系统,这一节用封装继承多态的思想封装这个任务,我们期望将Command以及任务类等统一封装到一个libCommand.dll中,对外只提供Command.h文件,外界无法知晓Command内部实现细节。
Command.h仅提供纯虚类的声明,以及唯一一个获取纯虚类指针的函数GetCommand
1 |
|
Command.cpp提供GetCommand的具体实现
1 |
|
Command.cpp中创建了不同的命令,并且定义了全局的TaskManager
重写纯虚类
命令的具体实现
1 | // |
TaskManager和之前一样,无需改动
1 | // |
具体实现
1 | // |
Task实现也没有改变
1 |
|
Command库编写
我们重写Command库的CMakeLists.txt
1 | cmake_minimum_required(VERSION 3.28) |
主函数调用
1 |
|
CMakeLists.txt编写
1 | cmake_minimum_required(VERSION 3.28) |
完结撒花
从2024年8月开始了第一个零基础C++视频的创作,历时七个多月完成了这一系列的创作,很多粉丝留言跟着我的节奏学会了C++基础用法,也能自己动手开发一些功能了,这让我很开心,比我自己学会了一套知识带来的快感还多。这套教程涵盖了C++98到C++20的内容,旨在帮助更多迷茫中找不到学习道路的人。
这套教程做完了,是结束也是新的开始,我会回归到聊天项目第二季的开发中,我觉得分享最好的状态就是陪伴,所以大家不用担心我更新完基础就离开。很多人问我为什么做免费教程,知识付费的时代做付费教程已成为大趋势,但我和别人不同的一点是我是真的喜欢技术的那类人,也喜欢把自己的认知分享出来,哪怕以后被时代淘汰,找不到合适的开发工作,甚至去做了其他行业,我也会在业余时间学习和分享编程技术,编程已经成为我生活中不可分割的一个乐趣。在分享中与大家交流,让我不再孤单,很多人素未相识,但是通过共同的兴趣走到一起,没有任何利益关系,这是一种君子之交淡如水的快乐,这种快乐和解忧杂货店那本书有些共鸣。
也祝福大家能学会C++并且找到合适的工作。再次感谢大家一路陪伴。 ————————-恋恋风辰 写于2025年3月初春