什么时候使用动态库和静态库

在c++中创建类库时,可以在动态(.dll.so)库和静态(.lib.a)库之间进行选择。它们之间的区别是什么?什么时候使用哪个比较合适?

280692 次浏览

如果您的库将在几个可执行文件之间共享,那么将其动态化以减少可执行文件的大小通常是有意义的。否则,一定要让它是静态的。

使用dll有几个缺点。装载和卸载它有额外的开销。还有一个额外的依赖关系。如果您更改dll使其与执行表不兼容,则它们将停止工作。另一方面,如果您更改了静态库,则使用旧版本编译的可执行文件将不会受到影响。

静态库被编译到客户机中。在编译时使用.lib,库的内容成为消费可执行文件的一部分。

动态库在运行时加载,而不是编译到客户端可执行文件中。动态库更加灵活,因为多个客户端可执行文件可以加载一个DLL并利用它的功能。这也将客户端代码的总体大小和可维护性保持在最小。

静态库增加了二进制文件中代码的大小。它们总是被加载,无论你用什么版本的代码编译,都是将运行的代码版本。

动态库的存储和版本是分开的。有可能加载的动态库版本不是随代码如果附带的原始版本,更新被认为与原始版本二进制兼容。

此外,动态库不一定被加载——它们通常在第一次调用时被加载——并且可以在使用相同库的组件之间共享(多个数据加载,一个代码加载)。

动态库在大多数时候被认为是更好的方法,但最初它们有一个重大缺陷(谷歌DLL地狱),这个缺陷几乎被最近的Windows操作系统(特别是Windows XP)所消除。

静态库必须链接到最终的可执行文件中;它成为可执行文件的一部分,并跟随它到任何地方。每次执行可执行文件时都会加载动态库,并以DLL文件的形式与可执行文件分开。

当您希望能够更改库提供的功能而不必重新链接可执行文件(只需替换DLL文件,而不必替换可执行文件)时,您将使用DLL。

当您没有理由使用动态库时,您可以使用静态库。

静态库是包含库的目标代码的存档,当链接到应用程序时,代码被编译成可执行文件。共享库的不同之处在于它们没有编译到可执行文件中。相反,动态链接器搜索一些目录寻找它需要的库,然后将其加载到内存中。 多个可执行文件可以同时使用同一个共享库,从而减少内存使用和可执行文件大小。然而,随后会有更多的文件与可执行文件一起分发。你需要确保库被安装到uses系统的某个地方,链接器可以找到它,静态链接消除了这个问题,但会导致更大的可执行文件

如果库是静态的,则在链接时将代码链接到可执行文件中。这使您的可执行文件更大(如果您走动态路线)。

如果库是动态的,那么在链接时,对所需方法的引用将内置于可执行文件中。这意味着您必须发布可执行文件和动态库。您还应该考虑对库中代码的共享访问是否安全、首选加载地址以及其他事项。

如果你能接受静态库,那就使用静态库。

库是绑定在应用程序可执行文件中的代码单元。

dll是可执行代码的独立单元。只有在对该代码进行调用时,才会在流程中加载它。一个dll可以被多个应用程序使用并加载在多个进程中,而在硬盘驱动器上仍然只有一个代码副本。

Dll优点:可用于在多个产品之间重用/共享代码;按需装入进程内存,不需要时可卸载;可以独立于程序的其余部分进行升级。

Dll缺点: dll加载和代码重基的性能影响;版本控制问题(“dll地狱”)

自由职业:没有性能影响,因为代码总是在进程中加载,不会重基;没有版本问题。

自由缺点:可执行文件/进程"bloat" -所有的代码都在你的可执行文件中,并在进程启动时加载;没有重用/共享-每个产品都有自己的代码副本。

如果你在嵌入式项目或专门的平台上工作,静态库是唯一的方法,而且很多时候它们编译到你的应用程序中不是那么麻烦。同时,拥有包含一切的项目和makefile会让生活更快乐。

您应该仔细考虑随时间的变化、版本控制、稳定性、兼容性等。

如果有两个应用程序使用共享代码,您是否希望强制这些应用程序一起更改,以防它们需要相互兼容?然后使用dll。所有的exe都将使用相同的代码。

或者你想把它们彼此隔离,这样你就可以改变一个,并确信你没有破坏另一个。然后使用静态库。

DLL地狱是当你可能应该使用一个静态库,但你使用了一个DLL代替,并不是所有的前任都与它兼容。

真正的权衡(在一个大型项目中)是在初始加载时间,库将在某个时间被链接,必须做出的决定是链接是否需要足够长的时间,以至于编译器需要咬紧牙关并提前完成,还是动态链接器可以在加载时完成。

其他人已经充分解释了静态库是什么,但我想指出一些使用静态库的注意事项,至少在Windows上:

  • 如果某些东西需要全局/静态且唯一,那么在将其放入静态库时要非常小心。如果多个dll链接到该静态库,它们将获得各自的单例副本。但是,如果您的应用程序是一个没有自定义dll的单一EXE,这可能不是问题。

  • 当你链接到静态库时,只有静态库中被你的DLL/EXE引用的部分会被链接到你的DLL/EXE中。

    例如,如果mylib.lib包含a.objb.obj,而您的DLL/EXE仅引用a.obj中的函数或变量,则整个b.obj将被链接器丢弃。如果b.obj包含全局/静态对象,则不会执行它们的构造函数和析构函数。如果这些构造函数/析构函数有副作用,您可能会对它们的缺失感到失望。

    同样地,如果静态库包含特殊的入口点,您可能需要注意是否实际包含了它们。在嵌入式编程(好吧,不是Windows)中的一个例子是一个被标记为位于特定地址的中断处理程序。您还需要将中断处理程序标记为入口点,以确保它不会被丢弃。

    这样做的另一个后果是,静态库可能包含由于未解析的引用而完全不可用的对象文件,但它不会导致链接器错误,直到从这些对象文件中引用函数或变量。

  • 您可能需要为每个静态库单独一个PDB,或者您可能希望将调试符号放在目标文件中,以便将它们滚入DLL/EXE的PDB中。Visual c++文档解释了的必要选项

  • 如果你将一个静态库链接到多个dll中,你可能会在同一个类中使用多个type_info对象。如果你的程序假设type_info是“单例”数据,并使用&typeid()type_info::before(),你可能会得到不期望的和令人惊讶的结果。

关于这个话题的精彩讨论,请阅读Sun的这篇文章

它包含了所有的好处,包括能够插入插入库。关于插入的更多细节可以在这篇文章中找到。

Ulrich Drepper关于“如何编写共享库”的论文也是一个很好的资源,详细介绍了如何最好地利用共享库,或他所说的“动态共享对象”(DSOs)。它更侧重于精灵二进制格式的共享库,但一些讨论也适用于Windows 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]>

在我们的项目中,我们使用了很多DLL(> 100)。这些DLL相互依赖,因此我们选择了动态链接的设置。然而,它有以下缺点:

  • 启动慢(> 10秒)
  • DLL必须进行版本控制,因为windows根据名称的唯一性加载模块。否则,自己编写的组件将得到错误版本的DLL(即已经加载的DLL而不是自己的分布式集)
  • 优化器只能在DLL边界内进行优化。例如,优化器试图将经常使用的数据和代码放在彼此旁边,但这将不能跨越DLL边界

也许更好的设置是使一切成为一个静态库(因此你只有一个可执行文件)。只有在没有发生代码复制的情况下,这才有效。一个测试似乎支持这个假设,但我找不到MSDN的官方引用。例如,创建1个exe:

  • Exe使用shared_lib1, shared_lib2
  • Shared_lib1使用shared_lib2
  • shared_lib2

shared_lib2的代码和变量应该只在最终合并的可执行文件中出现一次。有人能支持这个问题吗?

我给出一个一般的经验法则,如果你有一个很大的代码库,所有的代码库都建立在较低层次的库(例如Utils或Gui框架)之上,你想把它们划分成更易于管理的库,然后让它们成为静态库。动态库实际上不会为您带来任何东西,而且惊喜也更少——单个实例只有一个实例。

如果你有一个完全独立于其他代码库的库(例如第三方库),那么考虑将其作为一个dll。如果库是LGPL,由于许可条件,您可能无论如何都需要使用dll。

c++程序的构建分两个阶段

  1. 编译——生成目标代码(.obj)
  2. 链接——生成可执行代码(.exe或.dll)

静态库(.lib)只是一个.obj文件的包,因此不是一个完整的程序。它还没有经历构建程序的第二个(链接)阶段。另一方面,dll类似于exe,因此是完整的程序。

如果你构建了一个静态库,它还没有被链接,因此你的静态库的消费者将不得不使用与你使用的相同的编译器(如果你使用g++,他们将不得不使用g++)。

如果相反,你构建了一个dll(并将其构建为正确),你已经构建了一个所有消费者都可以使用的完整程序,无论他们使用哪种编译器。但是,如果需要跨编译器兼容性,则从dll导出有几个限制。

除了其他人提到的所有要点,我在特定的用例中使用静态库:

不允许我的终端用户访问一些通用库

换句话说,假设我的产品中有两个库,A和B。A使用B服务并依赖于它。但是B是一个通用库,包括许多可以单独使用的有用服务。为了避免我的终端用户直接从B中受益(他们应该为B的许可证付费!),我通常将B编译为一个静态库,并将其直接放在a中。因此,B服务对a来说是完全私有的,最终用户不能使用。