[译文] 编写 C++ 库(二): 实现

date
Apr 20, 2022
slug
translation-cpp-library-ii
status
Published
tags
Program
type
Post
pin
0
summary
编写 C++ 库的基本知识
本文是 编写 C++ 库(一): 设计 的后续文章.
在这一部分, 我们将介绍编写 C++ 库的基本知识.

2. 为 C++ 库编写代码的技术

C++ 提供了多种方式来编写库的代码:
notion image
其中, (I) 和 (IV) 可以打包成一个包 (.o / .a / .so), 而 (II) 和 (III) 必须作为源代码提供.

3. 打包库的代码的技术

注意: 本文接下来将解释创建一个库的技术细节. 这些例子和说明基于 Linux 操作系统, 使用 CMake 工具. 在 Windows 操作系统上的流程与之相似.
正如上文 "代码的使用方式" 一节所详细提到的, 一般来说, 用户主要有三种方式来使用这个库:
  1. 使用库的源代码: 这通常是和上文中 2. (III) 的编写方式一并使用, 因为 template 模板可以“按需”创建代码, 而你仅仅只是将源码包装进库中而已.
  1. 静态链接库: 库的代码将嵌入用户的程序, 成为其一部分 (库的代码将成为最后可执行文件的一部分).
    1. 这种库可以由以下的方式提供:
      • 一个 Object 文件 (.o)
      • 一系列 Object 文件 (称为一个 “Object 库”)
      • 一系列打包在一起的 Object 文件, 构成一个”主”文件 (.a) (称为一个 “Archive 库”)
  1. 动态链接库: 用户代码将存在“指向库的代码” (称为”符号 symbols”). 当运行用户代码的时候, 加载器 loader 会将库加载到内存中, 并提供对库的指向. 在这种情况下, 库提供的符号 symbols (应用二进制接口, Application Binary Interface, ABI) 定义了库和用户代码的结合.
    1. 如果库没能提供这些符号 symbols (可能在编译的时候, 由链接器 linker 负责; 也有可能在加载的时候, 由加载器 loader 负责), 我们会得到链接器 linker / 加载器 loader 的报错.
除此之外还有其它的使用方式, 不过它们在这篇文章中不会被讨论.
以上的这些方式都可以在库的 CMakeLists 文件中配置.
  1. 使用库的源代码: 创建一个 INTERFACE 库
    1. notion image
  1. 静态链接库:
      • .o (Object 库)
        • notion image
      • .a (Archive 库)
        • notion image
  1. 动态链接库: .so (共享对象, shared object)
    1. notion image
关于如何在 gcc 中手动创建每个选项的更多细节和技术规范, 请见:

4. 库和用户代码的结合

这就是说创建 main.out 可执行文件的过程.

a. 将库的源代码与用户代码结合

在编译源代码时, 最终产物只有 main.out 文件.
notion image
Preprocessor: 预处理 | Compiler: 编译器 | Linker: 链接器

b. 将库做为 objects (静态/动态库)

notion image
Declaration: 声明 | Definition: 定义
*重点: 在动态库 .so 的这种情况中, main.out 将不包含库代码编译出的二进制内容. 作为替代, 在 main.out 被运行时, 加载器 loader 将把 .so 加载到内存中.

5. 分发你的库的方式

C++ 提供了多种方式来引入一个库 (部分列表):
notion image
(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 包含了所有的代码)
与上表有关的更多信息:

6. 结语

当然, 还有更多的主题需要解决, 本文仅试图涵盖最基本和最常见的一些技术.
C++20 支持了 "可组合" 代码的新的结构形式, 它可以改变构建库的过程, 特别是, 消除了对头文件的需求. 这种形式被称为模块. 这是一个完全不同的话题, 需要由单独的文章另行说明.

感谢 Hana Dusíková 和 Billy Baker 审阅此文.
也感谢你的阅读, 我希望你觉得这篇文章对你有益 :)
更新 (2022年3月): 我创建了一个 repo: TestCMake, 其中有针对静态库和仅头文件库的 CMake 文件的简化示例 (基本与官方教程一致).

译者结: 本文是译者在完成一门课程作业时参考的文章. 本文介绍了创建一个 C++ 库的基本方式和类型, 对于初次编写 C++ 库的开发者而言十分友好, 故翻译后发布至译者博客.
 

© Beautyyu言醴 2022 - 2024