在 C++ 中创建类库时,您可以在动态 ( .dll
, .so
) 和静态 ( .lib
, .a
) 库之间进行选择。它们之间有什么区别,什么时候适合使用?
18 回答
静态库会增加二进制文件中代码的大小。它们总是被加载,并且您编译的任何版本的代码都是将运行的代码版本。
动态库是单独存储和版本化的。如果更新被认为与原始版本二进制兼容,那么加载的动态库版本可能不是您的代码随附的原始版本。
此外,动态库不一定要加载——它们通常在第一次调用时加载——并且可以在使用相同库的组件之间共享(多个数据加载,一个代码加载)。
大多数时候,动态库被认为是更好的方法,但最初它们有一个重大缺陷(谷歌 DLL 地狱),最近的 Windows 操作系统(尤其是 Windows XP)几乎消除了这个缺陷。
其他人已经充分解释了什么是静态库,但我想指出使用静态库的一些注意事项,至少在 Windows 上是这样:
单例:如果某些东西需要是全局/静态和唯一的,那么在将其放入静态库时要非常小心。如果多个 DLL 链接到该静态库,它们将各自获得自己的单例副本。但是,如果您的应用程序是没有自定义 DLL 的单个 EXE,则这可能不是问题。
未引用的代码删除:当您链接到静态库时,只有您的 DLL/EXE 引用的静态库部分才会链接到您的 DLL/EXE。
例如,如果
mylib.lib
containsa.obj
andb.obj
并且您的 DLL/EXE 仅引用来自 的函数或变量a.obj
,b.obj
则链接器将丢弃整个 。如果b.obj
包含全局/静态对象,它们的构造函数和析构函数将不会被执行。如果这些构造函数/析构函数有副作用,你可能会对它们的缺席感到失望。同样,如果静态库包含特殊入口点,您可能需要注意它们是否实际包含在内。嵌入式编程(好吧,不是 Windows)中的一个示例是标记为位于特定地址的中断处理程序。您还需要将中断处理程序标记为入口点,以确保它不会被丢弃。
这样做的另一个后果是静态库可能包含由于未解析的引用而完全不可用的目标文件,但在您从这些目标文件中引用函数或变量之前,它不会导致链接器错误。这可能会在编写库后很久才发生。
调试符号:您可能希望每个静态库都有一个单独的 PDB,或者您可能希望将调试符号放置在目标文件中,以便将它们滚动到 DLL/EXE 的 PDB 中。Visual C++ 文档解释了必要的选项。
RTTI:
type_info
如果将单个静态库链接到多个 DLL,则可能最终会为同一个类创建多个对象。如果您的程序假定这type_info
是“单例”数据并使用&typeid()
ortype_info::before()
,您可能会得到不希望的和令人惊讶的结果。
lib 是捆绑在应用程序可执行文件中的代码单元。
dll 是可执行代码的独立单元。只有在调用该代码时才会在进程中加载它。一个 dll 可以被多个应用程序使用并在多个进程中加载,同时在硬盘驱动器上仍然只有一份代码副本。
Dll 优点:可用于在多个产品之间重用/共享代码;按需加载进程内存,不需要时可以卸载;可以独立于程序的其余部分进行升级。
dll 缺点:dll 加载和代码变基对性能的影响;版本控制问题(“dll 地狱”)
Lib 优点:没有性能影响,因为代码总是在进程中加载并且不会重新定位;没有版本问题。
Lib cons:可执行文件/进程“膨胀” - 所有代码都在您的可执行文件中,并在进程启动时加载;不重复使用/共享 - 每个产品都有自己的代码副本。
C++ 程序分两个阶段构建
- 编译 - 生成目标代码 (.obj)
- 链接 - 生成可执行代码(.exe 或 .dll)
静态库 (.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 相互依赖,因此我们选择了动态链接的设置。但是它有以下缺点:
- 启动缓慢(> 10 秒)
- DLL 必须进行版本控制,因为 Windows 会根据名称的唯一性加载模块。否则自己编写的组件会得到错误的 DLL 版本(即已经加载的那个,而不是它自己的分布式集)
- 优化器只能在 DLL 边界内进行优化。例如,优化器尝试将经常使用的数据和代码并排放置,但这不适用于跨 DLL 边界
也许更好的设置是将所有内容都设为静态库(因此您只有一个可执行文件)。这仅在没有代码重复发生时才有效。一个测试似乎支持这个假设,但我找不到官方的 MSDN 报价。因此,例如使用以下命令制作 1 个 exe:
- exe 使用 shared_lib1、shared_lib2
- shared_lib1 使用 shared_lib2
- shared_lib2
shared_lib2 的代码和变量应该只出现在最终合并的可执行文件中一次。有人可以支持这个问题吗?
静态库是包含库的目标代码的档案,当链接到应用程序时,该代码被编译成可执行文件。共享库的不同之处在于它们不会编译到可执行文件中。相反,动态链接器搜索一些目录以查找它需要的库,然后将其加载到内存中。多个可执行文件可以同时使用同一个共享库,从而减少内存使用和可执行文件大小。但是,还有更多文件要与可执行文件一起分发。您需要确保将库安装到链接器可以找到它的使用系统上,静态链接消除了这个问题,但会导致更大的可执行文件。
如果您在嵌入式项目或专用平台上工作,静态库是唯一的出路,那么很多时候将它们编译到您的应用程序中也不会那么麻烦。还拥有包含所有内容的项目和生成文件会使生活更快乐。
我会给出一个一般的经验法则,如果你有一个大型代码库,所有代码库都构建在较低级别的库(例如 Utils 或 Gui 框架)之上,你希望将其划分为更易于管理的库,然后将它们设为静态库。动态库并没有真正为你买任何东西,而且惊喜也更少——例如,只有一个单例实例。
如果您有一个与代码库的其余部分完全分离的库(例如第三方库),请考虑将其设为 dll。如果库是 LGPL,由于许可条件,您可能仍需要使用 dll。