ChatGPT解决这个技术问题 Extra ChatGPT

何时使用动态库与静态库

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

应该注意的是,还有一个叫做“导入库”检查 stackoverflow.com/questions/3573475/…

O
Orion Adrian

静态库会增加二进制文件中代码的大小。它们总是被加载,并且您编译的任何版本的代码都是将运行的代码版本。

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

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

大多数时候,动态库被认为是更好的方法,但最初它们有一个重大缺陷(谷歌 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/… 允许位置独立代码的最大好处并不是 Linux 有什么特别之处,而是在 x64 指令集中添加了相对于 RIP 的寻址; Windows 和 Linux 都可以使用 RIP 相对寻址来减少重定位库时的修复次数。
b
bk1e

其他人已经充分解释了什么是静态库,但我想指出使用静态库的一些注意事项,至少在 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(),您可能会得到不希望的和令人惊讶的结果。


至于关于单例的一点,不要忘记一个 DLL 可能会被多次加载(相同版本或多个版本)并且仍然没有单例保证。
关于未引用代码删除的附加点:对 DLL 的调用也需要实际调用以强制加载引用的 DLL。将它添加为引用,但不包括任何引用它的调用仍然会得到与拥有不调用任何东西的静态库相同的结果。唯一的区别是实际发货的内容。在这两种情况下,静态构造函数和析构函数都不会触发。
@bk1e 那不应该发生。 .a 将始终包含它构建时使用的所有符号。当它静态链接到您的应用程序时,是的,只有那些使用的符号才会被链接。
我还不了解您的大部分帖子,但对我们 Windows 磨砂的特定内容 +1。
A
Anirudh Ramanathan

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

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

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

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

Lib 优点:没有性能影响,因为代码总是在进程中加载并且不会重新定位;没有版本问题。

Lib cons:可执行文件/进程“膨胀” - 所有代码都在您的可执行文件中,并在进程启动时加载;不重复使用/共享 - 每个产品都有自己的代码副本。


也可以在构建时使用 rebase.exe 或通过将 /BASE 选项传递给 link.exe 来完成变基。这是否有效取决于运行时是否有任何意外的地址空间冲突。
t
tcb

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 和客户端模块都必须使用相同的编译器和相同的构建设置
请研究如何通过构建动态库来实现交叉编译器兼容性。我记得你甚至会在 gcc 和 clang 中遇到问题。尽管他们尝试遵循相同的 ABI 标准。如果创建了一个真正的交叉编译器共享库,我只会说带有 C 签名的那个。
r
rburhum

除了静态库与动态库的技术含义(静态文件将所有内容捆绑在一个大二进制文件中与允许在多个不同可执行文件之间共享代码的动态库)之外,还有法律含义。

例如,如果您使用 LGPL 许可代码并且静态链接到 LGPL 库(并因此创建一个大二进制文件),您的代码将自动变为开源(free as in freedom) LGPL 代码。如果您链接到共享对象,那么您只需要对 LGPL 库本身进行的改进/错误修复。

例如,如果您决定如何编译移动应用程序,这将成为一个更为重要的问题(在 Android 中,您可以选择静态与动态,而在 iOS 中则没有 - 它始终是静态的)。


V
Vijay

创建静态库

$$:~/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]>

J
Jordan Parmer

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

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


s
spotcatbug

静态库必须链接到最终的可执行文件;它成为可执行文件的一部分,并随处可见。每次执行可执行文件时都会加载一个动态库,并作为 DLL 文件与可执行文件分开。

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

只要您没有理由使用动态库,您就会使用静态库。


当多个其他应用程序使用相同的功能时,您也可以使用 DLL - 这可以减少占用空间。
此外,扩展您的初始概念,“插件”架构,您希望稍后允许添加/未知功能而无需重新构建或重新发布,只能使用动态库来完成。
C
Corey Trager

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

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

或者您是否想将它们彼此隔离,以便您可以更改其中一个并确信您没有破坏另一个。然后使用静态库。

DLL 地狱是您可能应该使用静态库,但您使用的是 dll,并且并非所有 exe 都兼容它。


V
Void

Ulrich Drepper 关于“How to Write Shared Libraries”的论文也是很好的资源,详细介绍了如何最好地利用共享库,或者他所说的“动态共享对象”(DSO)。它更侧重于 ELF 二进制格式的共享库,但一些讨论也适用于 Windows DLL。


R
Rob Wells

有关该主题的精彩讨论,请阅读 Sun 的 this article

它涉及所有好处,包括能够插入插入库。可以在 this article here 中找到有关插入的更多详细信息。


p
pfranza

确实,您(在大型项目中)所做的权衡是在初始加载时间,库将在某个时间或另一个时间链接,必须做出的决定是链接是否需要足够长的时间以使编译器需要咬紧牙关提前做,或者动态链接器可以在加载时做。


D
Dima

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

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


S
Seb Rose

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

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

如果您可以使用静态库,请使用静态库。


g
gast128

我们在项目中使用了很多 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 的代码和变量应该只出现在最终合并的可执行文件中一次。有人可以支持这个问题吗?


您不是要以某种方式使用一些预编译器指令来避免代码重复吗?
Afaiac 预编译仅适用于每个模块(exe / dll /lib)基础。预编译主要是为了加快编译速度,尽管它也可以防止编译单元中的多个包含。但是,包含警卫是实现此效果的更好方法。
T
Terence Simpson

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


R
Robert Gould

如果您在嵌入式项目或专用平台上工作,静态库是唯一的出路,那么很多时候将它们编译到您的应用程序中也不会那么麻烦。还拥有包含所有内容的项目和生成文件会使生活更快乐。


t
the_mandrill

我会给出一个一般的经验法则,如果你有一个大型代码库,所有代码库都构建在较低级别的库(例如 Utils 或 Gui 框架)之上,你希望将其划分为更易于管理的库,然后将它们设为静态库。动态库并没有真正为你买任何东西,而且惊喜也更少——例如,只有一个单例实例。

如果您有一个与代码库的其余部分完全分离的库(例如第三方库),请考虑将其设为 dll。如果库是 LGPL,由于许可条件,您可能仍需要使用 dll。


G
Gupta

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

不允许我的最终用户访问我在代码中开发的一些通用库。

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