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

本文是 编写 C++ 库(一): 设计 的后续文章.

在这一部分, 我们将介绍编写 C++ 库的基本知识.


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

C++ 提供了多种方式来编写库的代码:

Untitled

其中, (I) 和 (IV) 可以打包成一个包 (.o / .a / .so), 而 (II) 和 (III) 必须作为源代码提供.

3. 打包库的代码的技术

注意: 本文接下来将解释创建一个库的技术细节. 这些例子和说明基于 Linux 操作系统, 使用 CMake 工具. 在 Windows 操作系统上的流程与之相似.

正如上文 "代码的使用方式" 一节所详细提到的, 一般来说, 用户主要有三种方式来使用这个库:

  1. 使用库的源代码: 这通常是和上文中 2. (III) 的编写方式一并使用, 因为 template 模板可以“按需”创建代码, 而你仅仅只是将源码包装进库中而已.

  2. 静态链接库: 库的代码将**嵌入用户的程序, 成为其一部分 (**库的代码将成为最后可执行文件的一部分).

    这种库可以由以下的方式提供:

    • 一个 Object 文件 (.o)
    • 一系列 Object 文件 (称为一个 “Object 库”)
    • 一系列打包在一起的 Object 文件, 构成一个”主”文件 (.a) (称为一个 “Archive 库”)
  3. 动态链接库: 用户代码将存在“指向库的代码” (称为”符号 symbols”). 当运行用户代码的时候, 加载器 loader 会将库加载到内存中, 并提供对库的指向. 在这种情况下, 库提供的符号 symbols (应用二进制接口, Application Binary Interface, ABI) 定义了库和用户代码的结合.

    如果库没能提供这些符号 symbols (可能在编译的时候, 由链接器 linker 负责; 也有可能在加载的时候, 由加载器 loader 负责), 我们会得到链接器 linker / 加载器 loader 的报错.

除此之外还有其它的使用方式, 不过它们在这篇文章中不会被讨论.

以上的这些方式都可以在库的 CMakeLists 文件中配置.

  1. 使用库的源代码: 创建一个 INTERFACE 库

    Untitled

  2. 静态链接库:

    • .o (Object 库)

      Untitled

    • .a (Archive 库)

      Untitled

  3. 动态链接库: .so (共享对象, shared object)

    Untitled

关于如何在 gcc 中手动创建每个选项的更多细节和技术规范, 请见:

Shared libraries with GCC on Linux

4. 库和用户代码的结合

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

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

在编译源代码时, 最终产物只有 main.out 文件.

Untitled

Preprocessor: 预处理 | Compiler: 编译器 | Linker: 链接器

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

Untitled

Declaration: 声明 | Definition: 定义

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

5. 分发你的库的方式

C++ 提供了多种方式来引入一个库 (部分列表):

Untitled

(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++ 库的开发者而言十分友好, 故翻译后发布至译者博客.