[译文] 编写 C++ 库(二): 实现
Link
本文是 编写 C++ 库(一): 设计 的后续文章.
在这一部分, 我们将介绍编写 C++ 库的基本知识.
2. 为 C++ 库编写代码的技术
C++ 提供了多种方式来编写库的代码:
其中, (I) 和 (IV) 可以打包成一个包 (.o / .a / .so), 而 (II) 和 (III) 必须作为源代码提供.
3. 打包库的代码的技术
注意: 本文接下来将解释创建一个库的技术细节. 这些例子和说明基于 Linux 操作系统, 使用 CMake 工具. 在 Windows 操作系统上的流程与之相似.
正如上文 "代码的使用方式" 一节所详细提到的, 一般来说, 用户主要有三种方式来使用这个库:
- 使用库的源代码: 这通常是和上文中 2. (III) 的编写方式一并使用, 因为 template 模板可以“按需”创建代码, 而你仅仅只是将源码包装进库中而已.
- 静态链接库: 库的代码将嵌入用户的程序, 成为其一部分 (库的代码将成为最后可执行文件的一部分).
- 一个 Object 文件 (.o)
- 一系列 Object 文件 (称为一个 “Object 库”)
- 一系列打包在一起的 Object 文件, 构成一个”主”文件 (.a) (称为一个 “Archive 库”)
这种库可以由以下的方式提供:
- 动态链接库: 用户代码将存在“指向库的代码” (称为”符号 symbols”). 当运行用户代码的时候, 加载器 loader 会将库加载到内存中, 并提供对库的指向. 在这种情况下, 库提供的符号 symbols (应用二进制接口, Application Binary Interface, ABI) 定义了库和用户代码的结合.
如果库没能提供这些符号 symbols (可能在编译的时候, 由链接器 linker 负责; 也有可能在加载的时候, 由加载器 loader 负责), 我们会得到链接器 linker / 加载器 loader 的报错.
除此之外还有其它的使用方式, 不过它们在这篇文章中不会被讨论.
以上的这些方式都可以在库的 CMakeLists 文件中配置.
- 使用库的源代码: 创建一个 INTERFACE 库
- 静态链接库:
- .o (Object 库)
- .a (Archive 库)
- 动态链接库: .so (共享对象, shared object)
关于如何在 gcc 中手动创建每个选项的更多细节和技术规范, 请见:
4. 库和用户代码的结合
这就是说创建 main.out 可执行文件的过程.
a. 将库的源代码与用户代码结合
在编译源代码时, 最终产物只有 main.out 文件.
Preprocessor: 预处理 | Compiler: 编译器 | Linker: 链接器
b. 将库做为 objects (静态/动态库)
Declaration: 声明 | Definition: 定义
*重点: 在动态库 .so 的这种情况中, main.out 将不包含库代码编译出的二进制内容. 作为替代, 在 main.out 被运行时, 加载器 loader 将把 .so 加载到内存中.
5. 分发你的库的方式
C++ 提供了多种方式来引入一个库 (部分列表):
(I) 静态库 (.o / .a): 被加入到程序 main 中
(II) 仅头文件库 (.h): 被加入到程序 main 中
(III) 动态库 (.so): 被加入到共享对象 mylib.so, 需要重启 main 程序.
(IV) 动态库 (.so): 无需重启程序 main, 只需重载库即可 — 通过使用 dlopen.
*重点: (III) 和 (IV) 的区别仅在于用户的使用方式, 体现在 main 的代码中 (而与库的代码无关).
以上这些方式的对比:
ㅤ | 静态库 .o / .a | 仅头文件库 | 动态库 .so | 插入式动态库 .so (由用户决定) |
程序大小 | 最小 (仅与相关的 .o 有关) | 由库中函数被调用的次数决定的 (因为函数是 inline 的) | 最大 (必须包含所有的 API) | 最大 (必须包含所有的 API) |
内存占用 | 单用户时 — 最小 (可执行文件中只有相关的 .o)
多用户时 — 因重复而翻倍 | 单用户时 — 最小 (可执行文件中只有相关的 .o)
多用户时 — 因重复而翻倍 | 最大, 但仅被加载一次. | 最大, 但仅被加载一次. |
暴露 API | 只有头文件中的 API | 所有的逻辑和 API | 只有头文件中的 API | 只有头文件中的 API |
是否需要重新编译 | 是 — .o 是程序 main 的一部分 | 是 — 源代码被用于创建程序 main | 否 — 不过仅当 .so 的符号 symbols (也就是 API) 没有改变的时候 | 否 — 不过仅当 .so 的符号 symbols (也就是 API) 没有改变的时候 |
库的更改 | main.out 可执行文件发生改变 | main.out 可执行文件发生改变 | 若 API 没有改变, 则仅有 mylib.so 发生改变 | 若 API 没有改变, 则仅有 mylib.so 发生改变 |
“最小” 意味着仅有用户使用到的部分 (如果是多个 .o 文件, 则仅有那些包含用户调用了的函数的 .o 文件)
“最大” 意味着库中的所有代码 (因为动态链接并不知道用户使用了库中的哪些部分, 因此 .so 包含了所有的代码)
与上表有关的更多信息:
- 相同的代码可以由不一样的方式创建: https://stackoverflow.com/questions/25606736/library-design-allow-user-to-decide-between-header-only-and-dynamically-linke
- 静态库与动态库的大小比较: https://stackoverflow.com/questions/27728385/how-statically-linked-binaries-could-be-smaller-than-dynamically-linked-binaries
6. 结语
当然, 还有更多的主题需要解决, 本文仅试图涵盖最基本和最常见的一些技术.
C++20 支持了 "可组合" 代码的新的结构形式, 它可以改变构建库的过程, 特别是, 消除了对头文件的需求. 这种形式被称为模块. 这是一个完全不同的话题, 需要由单独的文章另行说明.
感谢 Hana Dusíková 和 Billy Baker 审阅此文.
也感谢你的阅读, 我希望你觉得这篇文章对你有益 :)
译者结: 本文是译者在完成一门课程作业时参考的文章. 本文介绍了创建一个 C++ 库的基本方式和类型, 对于初次编写 C++ 库的开发者而言十分友好, 故翻译后发布至译者博客.