[英]When to use dynamic vs. static libraries
在 C++ 中创建 class 库时,您可以在动态( .dll
、 .so
)和 static( .lib
、 .a
)库之间进行选择。 它们之间有什么区别,什么时候适合使用哪个?
静态库会增加二进制代码的大小。 它们总是被加载,你编译的代码的任何版本都是将运行的代码版本。
动态库是单独存储和版本控制的。 如果更新被认为与原始版本二进制兼容,则加载的动态库版本可能不是随代码一起提供的原始版本。
此外,动态库不一定加载——它们通常在第一次调用时加载——并且可以在使用相同库的组件之间共享(多个数据加载,一个代码加载)。
大多数情况下,动态库被认为是更好的方法,但最初它们有一个主要缺陷(谷歌 DLL 地狱),最近的 Windows 操作系统(尤其是 Windows XP)几乎消除了这种缺陷。
其他人已经充分解释了什么是静态库,但我想指出使用静态库的一些注意事项,至少在 Windows 上:
单例:如果某些东西需要是全局的/静态的和唯一的,那么在将它放入静态库时要非常小心。 如果多个 DLL 链接到该静态库,则它们每个都将获得自己的单例副本。 但是,如果您的应用程序是没有自定义 DLL 的单个 EXE,则这可能不是问题。
未引用代码删除:当您链接静态库时,只有 DLL/EXE 引用的静态库部分才会链接到您的 DLL/EXE。
例如,如果mylib.lib
包含a.obj
和b.obj
并且您的 DLL/EXE 仅引用a.obj
函数或变量,则链接器将丢弃整个b.obj
。 如果b.obj
包含全局/静态对象,它们的构造函数和析构函数将不会被执行。 如果这些构造函数/析构函数有副作用,你可能会对它们的缺席感到失望。
同样,如果静态库包含特殊的入口点,您可能需要注意它们实际上是否包含在内。 在嵌入式编程(好吧,不是 Windows)中的一个例子是标记为位于特定地址的中断处理程序。 您还需要将中断处理程序标记为入口点,以确保它不会被丢弃。
另一个后果是静态库可能包含由于未解析的引用而完全无法使用的对象文件,但在您从这些对象文件中引用函数或变量之前,它不会导致链接器错误。 这可能会在编写库很久之后发生。
调试符号:您可能希望每个静态库都有一个单独的 PDB,或者您可能希望将调试符号放置在目标文件中,以便将它们卷入 DLL/EXE 的 PDB。 Visual C++ 文档解释了必要的选项。
RTTI:如果您将单个静态库链接到多个 DLL,您最终可能会为同一个类获得多个type_info
对象。 如果您的程序假定type_info
是“单例”数据并使用&typeid()
或type_info::before()
,您可能会得到不希望的和令人惊讶的结果。
lib 是捆绑在应用程序可执行文件中的代码单元。
dll 是一个独立的可执行代码单元。 只有在对该代码进行调用时才会在进程中加载它。 一个 dll 可以被多个应用程序使用并加载到多个进程中,同时硬盘驱动器上仍然只有一份代码副本。
Dll 优点:可用于在多个产品之间重用/共享代码; 按需加载进程内存,不需要时可以卸载; 可以独立于程序的其余部分进行升级。
Dll cons : dll 加载和代码变基对性能的影响; 版本问题(“dll 地狱”)
Lib 优点:没有性能影响,因为代码总是在进程中加载并且不会重新定位; 没有版本问题。
Lib 缺点:可执行文件/进程“膨胀” - 所有代码都在您的可执行文件中,并在进程启动时加载; 没有重用/共享 - 每个产品都有自己的代码副本。
C++ 程序分两个阶段构建
静态库 (.lib) 只是一堆 .obj 文件,因此不是一个完整的程序。 它尚未经历构建程序的第二(链接)阶段。 另一方面,DLL 类似于 exe,因此是完整的程序。
如果您构建一个静态库,它尚未链接,因此您的静态库的使用者将必须使用与您使用的相同的编译器(如果您使用 g++,他们将必须使用 g++)。
相反,如果您构建了一个 dll(并正确构建了它),那么您就构建了一个所有消费者都可以使用的完整程序,无论他们使用哪种编译器。 但是,如果需要交叉编译器兼容性,从 dll 导出有几个限制。
除了静态库与动态库的技术含义(静态文件将所有内容捆绑在一个大型二进制库与动态库中,允许在多个不同的可执行文件之间共享代码),还有法律含义。
例如,如果您使用 LGPL 许可代码并且您静态链接到 LGPL 库(从而创建一个大的二进制文件),您的代码将自动成为开源(免费的) LGPL 代码。 如果您链接到共享对象,那么您只需要 LGPL 对您对 LGPL 库本身所做的改进/错误修复。
例如,如果您决定如何编译您的移动应用程序(在 Android 中您可以选择静态与动态,而在 iOS 中您没有 - 它始终是静态的),这将成为一个重要得多的问题。
$$:~/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]>
静态库被编译到客户端。 .lib 在编译时使用,库的内容成为消费可执行文件的一部分。
动态库在运行时加载,而不是编译到客户端可执行文件中。 动态库更加灵活,因为多个客户端可执行文件可以加载 DLL 并利用其功能。 这也将客户端代码的整体大小和可维护性保持在最低限度。
您应该仔细考虑随时间的变化、版本控制、稳定性、兼容性等。
如果有两个应用程序使用共享代码,您是否要强制这些应用程序一起更改,以防它们需要相互兼容? 然后使用dll。 所有的 exe 都将使用相同的代码。
或者您想将它们彼此隔离,以便您可以更改其中一个并确信您没有破坏另一个。 然后使用静态库。
DLL 地狱是您可能应该使用静态库,但您使用的是 dll,并且并非所有的 exe 都与它兼容。
静态库必须链接到最终的可执行文件中; 它成为可执行文件的一部分,无论走到哪里都跟随它。 每次执行可执行文件时都会加载动态库,并作为 DLL 文件与可执行文件分开。
当您希望能够更改库提供的功能而无需重新链接可执行文件(只需替换 DLL 文件,而不必替换可执行文件)时,您将使用 DLL。
每当您没有理由使用动态库时,您就会使用静态库。
您(在大型项目中)所做的权衡实际上是在初始加载时间,库将在某个时间或其他时间链接,必须做出的决定是链接是否需要足够长的时间以供编译器需要硬着头皮在前面做,或者动态链接器可以在加载时做。
如果您的库要在多个可执行文件之间共享,通常可以使其动态化以减少可执行文件的大小。 否则,绝对让它静态。
使用 dll 有几个缺点。 加载和卸载它有额外的开销。 还有一个额外的依赖。 如果您更改 dll 以使其与您的可执行文件不兼容,它们将停止工作。 另一方面,如果您更改静态库,则使用旧版本编译的可执行文件不会受到影响。
如果库是静态的,那么在链接时代码会与您的可执行文件链接。 这会使您的可执行文件更大(比您使用动态路由时更大)。
如果库是动态的,则在链接时对所需方法的引用将内置到您的可执行文件中。 这意味着您必须发布可执行文件和动态库。 您还应该考虑对库中代码的共享访问是否安全,是否是其他内容的首选加载地址。
如果您可以使用静态库,请使用静态库。
我们在项目中使用了很多 DLL(> 100)。 这些 DLL 相互依赖,因此我们选择了动态链接的设置。 但是它有以下缺点:
也许更好的设置是将所有内容都设为静态库(因此您只有一个可执行文件)。 这仅在没有发生代码重复的情况下才有效。 一个测试似乎支持这个假设,但我找不到官方的 MSDN 引用。 因此,例如使用以下命令制作 1 个 exe:
shared_lib2 的代码和变量应该只出现在最终合并的可执行文件中一次。 任何人都可以支持这个问题吗?
静态库是包含库目标代码的档案,当链接到应用程序时,该代码被编译成可执行文件。 共享库的不同之处在于它们不会编译成可执行文件。 相反,动态链接器搜索一些目录以寻找它需要的库,然后将其加载到内存中。 多个可执行文件可以同时使用同一个共享库,从而减少内存使用和可执行文件大小。 但是,还有更多文件要与可执行文件一起分发。 您需要确保将库安装到使用系统上链接器可以找到它的某个位置,静态链接消除了这个问题,但会导致更大的可执行文件。
如果您在嵌入式项目或专用平台静态库上工作是唯一的出路,那么很多时候将它们编译到您的应用程序中也不那么麻烦。 还拥有包含所有内容的项目和 makefile 让生活更快乐。
我会给出一个一般的经验法则,如果您有一个大型代码库,所有代码库都构建在较低级别的库(例如,Utils 或 Gui 框架)之上,您希望将其划分为更易于管理的库,然后将它们设为静态库。 动态库并没有真正给你买任何东西,而且惊喜也更少——例如,只有一个单例实例。
如果您的库与代码库的其余部分(例如第三方库)完全分开,请考虑将其设为 dll。 如果库是 LGPL,由于许可条件,您可能无论如何都需要使用 dll。
除了其他人提到的所有要点之外,我在特定用例中使用 static 库来:
不允许我的最终用户访问我在代码中开发的一些通用库。
换句话说,假设我的产品中有两个库,A 和 B。A 使用 B 服务并依赖它。 但是 B 是一个通用库,包括许多可以单独使用的有用服务。 为了避免我的最终用户直接从 B 中受益(他们应该为其许可证付费)。 我一般把B编译成static的库,直接放到A里面,结果。 B 服务对 A 来说是完全私有的,最终用户无法使用。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.