何时使用动态库与静态库
Posted
技术标签:
【中文标题】何时使用动态库与静态库【英文标题】:When to use dynamic vs. static libraries 【发布时间】:2010-09-13 11:51:59 【问题描述】:在 C++ 中创建类库时,您可以在动态(.dll
、.so
)和静态(.lib
、.a
)库之间进行选择。它们有什么区别,什么时候用哪个合适?
【问题讨论】:
需要注意的是还有一个叫做“导入库”的检查***.com/questions/3573475/… 【参考方案1】:静态库会增加二进制文件中代码的大小。它们始终会被加载,并且您编译时使用的任何版本的代码都是将要运行的代码版本。
动态库单独存储和版本控制。 如果更新被认为与原始版本二进制兼容,则可能会加载不是您的代码随附的原始版本的动态库版本。
另外,动态库不一定要加载——它们通常在第一次调用时加载——并且可以在使用相同库的组件之间共享(多个数据加载,一个代码加载)。
大多数时候,动态库被认为是更好的方法,但最初它们存在一个重大缺陷(谷歌 DLL 地狱),该缺陷已被更新的 Windows 操作系统(尤其是 Windows XP)几乎消除。
【讨论】:
在 Windows/Mac(没有包管理器)上,确实没有充分的理由使用动态库而不是静态库。由于 Windows DLL 不可重定位,因此代码共享通常不起作用(并且通常每个应用程序都会发布并使用自己的库版本)。唯一真正的好处是更新库更容易。 在mac上,我用了很多动态库。例如,mac os x 嵌入了 sqlite3。我创建了一个具有用于性能存储的 sqlite3 数据库功能的程序。然而,因为它很少使用动态链接节省了编译时间,使测试更容易/更快但是,如果我要构建一个发布版本,我想我会一直使用静态库,以防出现兼容性问题 @Zifre: relocatable = 可以加载到不同的虚拟地址。 DLL 当然支持这一点。 @dma_k:Windows DLL 可以加载到不同的地址,但这只是因为链接器复制了所有代码并更改了地址编号。对于共享对象,所有地址引用都是相对的,因此多个进程可以为共享对象共享相同的内存。换句话说,在 Windows 上,3 个程序使用的 1 MB DLL = 3 MB。在 Linux 上,3 个程序使用的 A MB SO = 1 MB。 Windows 和 Linux 都有共享库的加载时间重定位的概念 eli.thegreenplace.net/2011/08/25/… x64指令集; Windows 和 Linux 都可以利用 RIP 相对寻址确实减少了重定位库时的修复次数。【参考方案2】:其他人已经充分解释了什么是静态库,但我想指出使用静态库的一些注意事项,至少在 Windows 上是这样:
Singletons:如果需要全局/静态和唯一的东西,在将其放入静态库时要非常小心。如果多个 DLL 链接到该静态库,它们将各自获得自己的单例副本。但是,如果您的应用程序是没有自定义 DLL 的单个 EXE,则这可能不是问题。
未引用的代码删除:当您链接到静态库时,只有您的 DLL/EXE 引用的静态库部分才会链接到您的 DLL/EXE。
例如,如果mylib.lib
包含a.obj
和b.obj
并且您的DLL/EXE 仅引用来自a.obj
的函数或变量,则整个b.obj
将被链接器丢弃。如果b.obj
包含全局/静态对象,它们的构造函数和析构函数将不会被执行。如果这些构造函数/析构函数有副作用,你可能会对它们的缺席感到失望。
同样,如果静态库包含特殊入口点,您可能需要注意它们是否实际包含在内。嵌入式编程(好吧,不是 Windows)中的一个示例是标记为位于特定地址的中断处理程序。您还需要将中断处理程序标记为入口点,以确保它不会被丢弃。
这样做的另一个后果是静态库可能包含由于未解析的引用而完全不可用的目标文件,但在您从这些目标文件中引用函数或变量之前,它不会导致链接器错误。这可能会在编写库很久之后发生。
调试符号:您可能希望每个静态库都有一个单独的 PDB,或者您可能希望将调试符号放置在目标文件中,以便它们滚动到 PDB 中DLL/EXE。 Visual C++ 文档解释了the necessary options。
RTTI:如果您将单个静态库链接到多个 DLL,您最终可能会为同一个类创建多个 type_info
对象。如果您的程序假定type_info
是“单例”数据并使用&typeid()
或type_info::before()
,您可能会得到不希望的和令人惊讶的结果。
【讨论】:
关于单例的一点,不要忘记一个DLL可能会被多次加载(相同版本或多个版本)并且仍然没有单例保证。 关于未引用代码删除的附加点:对 DLL 的调用还需要实际调用以强制加载引用的 DLL。将其添加为引用,但不包括任何引用它的调用仍然会得到与拥有不调用任何内容的静态库相同的结果。唯一的区别是实际发货的内容。在这两种情况下,静态构造函数和析构函数都不会触发。 @bk1e 这不应该发生。 .a 将始终包含它构建时使用的所有符号。当它静态链接到您的应用程序时,是的,只有那些使用的符号才会被链接。【参考方案3】:lib 是捆绑在应用程序可执行文件中的代码单元。
dll 是可执行代码的独立单元。仅当对该代码进行调用时,它才会在进程中加载。一个 dll 可以被多个应用程序使用并在多个进程中加载,同时在硬盘上仍然只有一个代码副本。
Dll 专业人士:可用于在多个产品之间重用/共享代码;按需加载进程内存,不需要时可以卸载;可以独立于程序的其余部分进行升级。
Dll 缺点:dll 加载和代码变基对性能的影响;版本控制问题(“dll 地狱”)
Lib 专业人士:不会影响性能,因为代码总是在进程中加载并且不会重新定位;没有版本问题。
Lib cons:可执行文件/进程“膨胀” - 所有代码都在您的可执行文件中,并在进程启动时加载;没有重复使用/共享 - 每个产品都有自己的代码副本。
【讨论】:
变基也可以在构建时使用 rebase.exe 或通过将 /BASE 选项传递给 link.exe 来完成。这是否有效取决于运行时是否有任何意外的地址空间冲突。【参考方案4】:C++ 程序分为两个阶段构建
-
编译 - 生成目标代码 (.obj)
链接 - 生成可执行代码(.exe 或 .dll)
静态库 (.lib) 只是一组 .obj 文件,因此不是一个完整的程序。它还没有经历构建程序的第二(链接)阶段。另一方面,Dll 类似于 exe,因此是完整的程序。
如果您构建一个静态库,它还没有被链接,因此您的静态库的使用者必须使用与您使用的编译器相同的编译器(如果您使用 g++,他们将不得不使用 g++)。
如果您构建了一个 dll(并构建了它correctly),那么您构建了一个所有消费者都可以使用的完整程序,无论他们使用哪种编译器。但是,如果需要交叉编译器兼容性,则从 dll 导出时有一些限制。
【讨论】:
这对我来说是个新闻。使用 DLL 时,交叉编译器有哪些限制?让程序员在不需要相同工具链的情况下进行构建似乎是 DLL 的一大优势 这个答案很丰富。添加小警告:consumers of your static library will have to use the same compiler that you used
如果静态库使用 C++ 库,例如 #include <iostream>
。
除非使用相同的编译器,否则无法使用 c++ dll(因为没有标准的 c++ abi,符号以不同的方式被破坏)。 dll 和客户端模块都必须使用相同的编译器和相同的构建设置【参考方案5】:
除了静态库与动态库的技术影响(静态文件将所有内容捆绑在一个大二进制文件中,而动态库允许在多个不同的可执行文件之间共享代码)之外,还有法律影响。
例如,如果您使用 LGPL 许可代码并且静态链接到 LGPL 库(从而创建一个大的二进制文件),您的代码将自动变为开源(free as in freedom) LGPL 代码。如果您链接到共享对象,那么您只需要将您对 LGPL 库本身所做的改进/错误修复进行 LGPL。
例如,如果您决定如何编译移动应用程序,这将成为一个更为重要的问题(在 android 中,您可以选择静态还是动态,而在 iOS 中则没有 - 它始终是静态的)。
【讨论】:
【参考方案6】:创建静态库
$$:~/static [32]> cat foo.c
#include<stdio.h>
void foo()
printf("\nhello world\n");
$$:~/static [33]> cat foo.h
#ifndef _H_FOO_H
#define _H_FOO_H
void foo();
#endif
$$:~/static [34]> cat foo2.c
#include<stdio.h>
void foo2()
printf("\nworld\n");
$$:~/static [35]> cat foo2.h
#ifndef _H_FOO2_H
#define _H_FOO2_H
void foo2();
#endif
$$:~/static [36]> cat hello.c
#include<foo.h>
#include<foo2.h>
void main()
foo();
foo2();
$$:~/static [37]> cat makefile
hello: hello.o libtest.a
cc -o hello hello.o -L. -ltest
hello.o: hello.c
cc -c hello.c -I`pwd`
libtest.a:foo.o foo2.o
ar cr libtest.a foo.o foo2.o
foo.o:foo.c
cc -c foo.c
foo2.o:foo.c
cc -c foo2.c
clean:
rm -f foo.o foo2.o libtest.a hello.o
$$:~/static [38]>
创建动态库
$$:~/dynamic [44]> cat foo.c
#include<stdio.h>
void foo()
printf("\nhello world\n");
$$:~/dynamic [45]> cat foo.h
#ifndef _H_FOO_H
#define _H_FOO_H
void foo();
#endif
$$:~/dynamic [46]> cat foo2.c
#include<stdio.h>
void foo2()
printf("\nworld\n");
$$:~/dynamic [47]> cat foo2.h
#ifndef _H_FOO2_H
#define _H_FOO2_H
void foo2();
#endif
$$:~/dynamic [48]> cat hello.c
#include<foo.h>
#include<foo2.h>
void main()
foo();
foo2();
$$:~/dynamic [49]> cat makefile
hello:hello.o libtest.sl
cc -o hello hello.o -L`pwd` -ltest
hello.o:
cc -c -b hello.c -I`pwd`
libtest.sl:foo.o foo2.o
cc -G -b -o libtest.sl foo.o foo2.o
foo.o:foo.c
cc -c -b foo.c
foo2.o:foo.c
cc -c -b foo2.c
clean:
rm -f libtest.sl foo.o foo
2.o hello.o
$$:~/dynamic [50]>
【讨论】:
【参考方案7】:静态库被编译到客户端。 .lib 在编译时使用,库的内容成为消费可执行文件的一部分。
动态库在运行时加载,而不是编译到客户端可执行文件中。动态库更加灵活,因为多个客户端可执行文件可以加载 DLL 并利用其功能。这也将您的客户端代码的整体大小和可维护性保持在最低限度。
【讨论】:
【参考方案8】:您应该仔细考虑随着时间的推移而发生的变化、版本控制、稳定性、兼容性等。
如果有两个应用程序使用共享代码,是否要强制这些应用程序一起更改,以防它们需要相互兼容?然后使用dll。所有的 exe 都将使用相同的代码。
或者你想将它们彼此隔离,这样你就可以改变一个并确信你没有破坏另一个。然后使用静态库。
DLL 地狱是指您可能应该使用静态库,但您使用的是 dll,并且并非所有 exe 都兼容它。
【讨论】:
【参考方案9】:静态库必须链接到最终的可执行文件;它成为可执行文件的一部分,并随处可见。每次执行可执行文件时都会加载一个动态库,并作为 DLL 文件与可执行文件分开。
如果您希望能够更改库提供的功能而无需重新链接可执行文件(只需替换 DLL 文件,而不必替换可执行文件),您将使用 DLL。
只要您没有理由使用动态库,您就会使用静态库。
【讨论】:
当多个其他应用程序使用相同的功能时,您也可以使用 DLL - 这可以减少占用空间。 此外,扩展您的初始概念,“插件”架构,您希望稍后允许添加/未知功能而无需重新构建或重新发布,只能使用动态库来完成。【参考方案10】:Ulrich Drepper 关于“How to Write Shared Libraries”的论文也是很好的资源,详细介绍了如何最好地利用共享库,或者他所说的“动态共享对象”(DSO)。它更侧重于ELF 二进制格式的共享库,但有些讨论也适用于 Windows DLL。
【讨论】:
【参考方案11】:有关此主题的精彩讨论,请阅读来自 Sun 的 this article。
它具有所有好处,包括能够插入插入库。更多关于插入的细节可以在this article here找到。
【讨论】:
【参考方案12】:实际上,您(在大型项目中)所做的权衡是在初始加载时间,库将在某个时间或另一个时间链接,必须做出的决定是链接是否需要足够长的时间编译器需要咬紧牙关提前完成,或者动态链接器可以在加载时完成。
【讨论】:
【参考方案13】:如果您的库要在多个可执行文件之间共享,通常将其动态化以减小可执行文件的大小是有意义的。否则,请务必将其设为静态。
使用 dll 有几个缺点。加载和卸载它有额外的开销。还有一个额外的依赖。如果您更改 dll 以使其与您的可执行文件不兼容,它们将停止工作。另一方面,如果您更改静态库,则使用旧版本编译的可执行文件不会受到影响。
【讨论】:
【参考方案14】:如果库是静态的,那么在链接时代码会与您的可执行文件链接。这使您的可执行文件更大(比您走动态路线时)。
如果库是动态的,那么在链接时对所需方法的引用将内置到您的可执行文件中。这意味着您必须交付可执行文件和动态库。您还应该考虑对库中代码的共享访问是否安全、首选加载地址等。
如果您可以使用静态库,请使用静态库。
【讨论】:
【参考方案15】:我们在项目中使用了很多 DLL (> 100)。这些 DLL 相互依赖,因此我们选择了动态链接的设置。但是它有以下缺点:
启动缓慢(> 10 秒) 必须对DLL 进行版本控制,因为Windows 会根据名称的唯一性加载模块。否则自己编写的组件会得到错误版本的 DLL(即已经加载的那个,而不是它自己的分布式集) 优化器只能在 DLL 边界内优化。例如,优化器尝试将经常使用的数据和代码并排放置,但这不会跨越 DLL 边界也许更好的设置是使 everything 成为静态库(因此您只有一个可执行文件)。这仅在没有代码重复发生时才有效。一个测试似乎支持这个假设,但我找不到官方的 MSDN 报价。因此,例如使用以下命令制作 1 个 exe:
exe 使用 shared_lib1、shared_lib2 shared_lib1 使用 shared_lib2 shared_lib2shared_lib2 的代码和变量应该只出现在最终合并的可执行文件中一次。有人可以支持这个问题吗?
【讨论】:
你不是打算以某种方式使用一些预编译器指令来避免代码重复吗? Afaiac 预编译仅适用于每个模块(exe / dll /lib)基础。预编译主要是为了加快编译速度,尽管它也可以防止编译单元中的多个包含。但是包含守卫是实现此效果的更好方法。【参考方案16】:静态库是包含库的目标代码的档案,当链接到应用程序时,该代码被编译成可执行文件。共享库的不同之处在于它们不会编译到可执行文件中。相反,动态链接器搜索一些目录以查找它需要的库,然后将其加载到内存中。 多个可执行文件可以同时使用同一个共享库,从而减少内存使用和可执行文件大小。但是,还有更多文件要与可执行文件一起分发。您需要确保将库安装到链接器可以找到它的使用系统上,静态链接消除了这个问题,但会导致更大的可执行文件。
【讨论】:
【参考方案17】:如果您从事嵌入式项目或专用平台静态库是唯一的出路,那么很多时候将它们编译到您的应用程序中也不会那么麻烦。还拥有包含所有内容的项目和 makefile 让生活更快乐。
【讨论】:
【参考方案18】:我会给出一个通用的经验法则,如果您有一个大型代码库,所有代码库都构建在较低级别的库(例如 Utils 或 Gui 框架)之上,您希望将其划分为更易于管理的库,然后将它们设为静态图书馆。动态库并没有真正为您买任何东西,而且惊喜也更少——例如,只有一个单例实例。
如果您有一个与代码库的其余部分完全分离的库(例如第三方库),请考虑将其设为 dll。如果库是 LGPL,由于许可条件,您可能仍需要使用 dll。
【讨论】:
以上是关于何时使用动态库与静态库的主要内容,如果未能解决你的问题,请参考以下文章