文章预览:

  • 一. 前言
  • 二. 动态库的加载配置
  • 三. 如何使用 LIB 文件解析 DLL 函数和数据
  • 四. 相关场景分析库的使用



一. 前言

在现代软件开发中,库(Library)是重用代码的关键工具。通常我们会遇到两种库类型:动态链接库(DLL)和静态链接库(LIB)。理解它们的区别以及如何管理它们的版本是确保软件稳定性的关键。

动态链接库(DLL): 动态链接库在程序运行时被加载,可以在多个程序之间共享。它的好处在于能够减少内存占用并支持模块化更新。DLL 文件通常与一个导入库(.lib 文件)配对使用,后者仅用于编译阶段,帮助链接器找到 DLL 中的符号。

静态链接库(LIB): 静态链接库在编译时将库的实现代码直接包含到你的程序中。这种情况下,最终生成的可执行文件会包含所有必要的库代码。这意味着每个使用该库的程序都有自己的副本,增加了程序的大小,但避免了运行时的外部依赖。


二. 动态库的加载配置

在Visual Studio中使用外部第三方库时,通常需要提供三个关键文件或组件:DLL(动态链接库)LIB(静态库或导入库) 以及 接口头文件

每个部分的作用和缺少某一项可能导致的问题如下:

1、DLL(动态链接库) — 运行时加载

1)作用DLL 文件包含了库的实际实现代码。运行时,程序会动态加载这个库以调用其提供的功能

2)缺少 DLL 的问题:

  • 运行时错误: 如果你的程序在运行时找不到所需的 DLL 文件,通常会导致程序崩溃或报错,提示找不到 DLL。比如,Windows 系统会弹出 "无法启动程序,因为计算机中丢失了 [DLL 名称]" 的错误信息。
  • 依赖性问题: 即使在编译时能够找到 LIB 文件,程序运行时仍需要 DLL 才能正确执行。如果缺少 DLL,程序无法完成预期的功能。

2、LIB(这里指导入库)

1)作用:帮助链接器在编译阶段找到并解析 DLL(动态链接库)中的函数实现部分进行链接。

2)缺少 LIB 的问题:

  • 编译错误: 如果编译器找不到 LIB 文件(在使用 DLL 时),会导致链接错误。编译器无法解析符号,提示找不到某些函数或变量。

3、接口头文件(Header Files)

1)作用: 接口头文件定义了库的 API(应用程序接口),即库提供的函数、类和数据结构的声明。它们告诉编译器如何正确地调用库中的实现代码。

2)缺少头文件的情况:

  • 编译错误: 如果缺少头文件,编译器无法找到函数、类和数据结构的声明,编译器会报出未定义标识符等错误。编译器不知道如何处理库的相关功能。
  • 代码不兼容: 即使 LIB 和 DLL 存在,但如果头文件不匹配,可能会导致函数调用不正确,程序崩溃或产生未定义行为。

三. 如何使用 LIB 文件解析 DLL 函数和数据

1)函数和数据的声明

在编写代码时,你需要知道 DLL 提供哪些函数和数据。为此,你会包括一个头文件,这个头文件中声明了 DLL 的函数和数据。

例如:

// example.h
#ifdef EXAMPLE_EXPORTS
#define EXAMPLE_API __declspec(dllexport)
#else
#define EXAMPLE_API __declspec(dllimport)
#endif

EXAMPLE_API void exampleFunction();

在这个例子中,__declspec(dllimport) 指示编译器这是一个从 DLL 中导入的函数。

2)编译阶段

  • 包含头文件: 在编译程序时,包含该头文件,编译器使用头文件中的声明(例如函数原型)来生成对DLL函数的调用指令。头文件告诉编译器哪些函数会被调用,但没有提供实际的实现。
  • 使用 LIB 文件进行链接: 在链接阶段,链接器会使用 LIB 文件来解析 DLL 的符号。LIB 文件中包含了 DLL 中导出的函数和数据的地址信息。具体来说:
    ① 符号表: LIB 文件包含一个符号表,其中列出了所有的函数和数据符号。每个符号都有一个地址,它是实际函数或数据在 DLL 中的导入位置的标识。
    ② 导入描述表: LIB 文件会包含对这些符号的引用描述。链接器根据这些描述,将你的代码中的符号引用与 DLL 中的代码实现部分进行链接。

例如,如果你的代码调用了 exampleFunction(),编译器会生成一个对该函数的调用,而链接器会查找 LIB 文件中的符号表,找到 exampleFunction 的导入地址,并将其与 DLL 中的实际地址进行关联。在链接阶段,链接器会使用 LIB 文件来解析 DLL 的符号。LIB 文件中包含了 DLL 中导出的函数和数据的地址信息

3)运行时链接

  • 加载 DLL: 当程序运行时,操作系统会加载 DLL 文件到内存中。如果 DLL 文件不在预期的位置,程序将无法启动。
  • [Library] 动态库的使用及其版本管理_重新编译

  • 附加库目录告诉链接器在哪里搜索相应的dll
  • 动态链接 — 附加依赖项: 在程序运行期间,操作系统在运行库时将使用 LIB 文件中的信息来解析函数调用。如果函数调用在DLL中找不到,程序会产生错误。
    附加依赖项通过在附加依赖项中指定这些库文件名称,链接器就会在链接过程中自动尝试链接这些包含项目所需的函数实现、对象定义等的DLL库文件(附加依赖项中指定的库文件名称必须与附加库目录中实际存在的库文件名称相匹配(忽略文件扩展名))。

4)举例说明

假设你有一个 DLL 文件 example.dll,以及一个对应的 LIB 文件 example.lib 和头文件 example.h。你在代码中调用了 exampleFunction()

  • 头文件 example.h 里声明了 exampleFunction()
  • example.lib 文件包含了 example.dll 中 exampleFunction 的符号信息。这个 LIB 文件在链接时提供了 exampleFunction 的位置。
  • example.dll 在运行时提供了 exampleFunction 的实际实现。程序运行时,操作系统会确保 DLL 被正确加载,并将调用转发到 DLL 中的实际代码。

四. 相关场景分析库的使用

1、 已知:软件和视觉库都依赖于日志库,同时软件也依赖视觉库
问题:如果日志库函数实现发生了改变,但接口并没有变,那么软件在加载了最新的日志库之后,视觉库会自动使用最新的日志库吗?还是视觉库要加载最新日志库重新编译

回答
动态链接库的行为
1)软件: 加载最新的日志库时,会调用日志库中最新的函数实现,只要这些函数的接口保持不变。
2)视觉库: 作为一个依赖于日志库的组件,也会使用最新的日志库实现,因为它是在运行时动态加载的。

总结:如果日志库的接口没有改变,且函数实现的改变没有引入破坏性的变化软件和视觉库应该能够使用最新的日志库实现。但为了确保所有组件的兼容性和稳定性,建议在更新日志库后进行全面的测试,并在需要时重新编译相关组件。

2、已知:有视觉库、相机库和日志库,三者的关系是日志库依赖于相机库和视觉库相机库依赖于视觉库
问题:如果日志库的接口发生的改变,我们知道视觉库需要加载最新的日志库并重新编译,这时如果相机库也加载了最新的日志库并重新编译,那么视觉库还需要因为相机库的重新编译而重新编译吗?

回答
1)日志库接口的改变可能会影响任何依赖于它的库,包括相机库和视觉库。所以两者都重新编译是正确的。
2)如果相机库重新编译并且其编译结果没有改变视觉库的接口要求,那么视觉库通常需要重新编译因为视觉库的接口没有改变

实际的编译需求

1、一个库(视觉库)是否需要重新编译主要看其接口或函数实现是否发生改变,或者它依赖的库(相机库、日志库)接口是否发生改变,若它依赖的库接口未发生改变,即使重新进行了编译,该库(视觉库)也无须重新编译;

2、当一个库的函数实现发生改变,而接口保持不变时,是否需要重新编译依赖该库的其他库取决于几个因素:

  • 被依赖库(相机库或日志库为例):
    如果被依赖库的函数实现发生变化,但接口没有改变,通常情况下,被依赖库(相机库或日志库)本身需要重新编译。这是因为函数实现的变化可能会影响到库的内部状态或优化,确保新实现被正确集成和测试。
  • 依赖库:依赖库(视觉库为例)通常不需要重新编译,前提是:
    接口没有变化:依赖库调用的接口和函数签名未改变,依赖库对这些接口的调用方式和预期没有受到影响。
    函数调用方式未改变:即使被依赖库的内部实现变化,只要它的接口保持不变,依赖库的调用方式仍然有效,通常依赖库无需重新编译。

下雨天,最惬意的事莫过于躺在床上静静听雨,雨中入眠,连梦里也长出青苔。