C++中编译静态库与动态库

news2024/11/24 14:32:30

1.库的理解

库就是写好的现有的,成熟的,可复用的代码。现实中每个程序都要依赖很多基础的底层库,不可能每个人的代码都从零开始,因此库的存在意义非同寻常。

本质上来说库是一种可执行代码的二进制形式是预编译代码的集合,可以被程序重新使用,能够被操作系统载入内存执行。

库有两种:静态库(.a、.lib)和动态库(.so、.dll)。所谓静态、动态是指链接。具体来说,二者链接的时间点不同,代码被载入的时刻不同,具体详见下文。

回顾一下,将一个程序编译成可执行程序的步骤:

2.源文件、编译、链接到可执行文件

源代码要经过上图中预编译(Processing)、编译(Compilation)、汇编(Assembly)、链接(Linking)等步骤生成可执行文件。

2.1编译

(1)预编译,即预处理,主要处理在源代码文件中以“#”开始的预编译指令,如宏展开、处理条件编译指令、处理#include指令等。

(2)编译过程就是把预处理完的文件进行一系列词法分析、语法分析、语义分析以及优化后生成相应的汇编代码文件。

(3)汇编是将汇编代码转变成机器可以执行的指令。

至此,C/C++源代码文件经过预编译、编译和汇编直接输出目标文件(.o文件)。这个过程也就是编译器所做的事(即将高级语言翻译成机器语言),比如我们用C/C+语言写的一个程序可以使用编译器将其翻译成机器可以执行的指令及数据。

现代的编译器将一个源代码文件编译成一个未链接的目标文件,然后由链接器最终将这些目标文件链接起来形成可执行文件。

2.2链接

程序设计的模块化是人们一直在追求的目标,因为当一个系统十分复杂的时候,我们不得不将一个复杂的系统逐步分割成多个小的系统以达到各个突破的目的。一个复杂的软件也是如此,人们把每个源代码模块独立地编译,然后按照需要将它们“组装”起来,这个组装模块的过程就是链接(Linking),链接的主要内容就是把各个模块之间相互引用的部分都处理好,使得各个模块之间能够正确地衔接。

3.静态链接与动态链接

3.1静态链接

最基本的静态链接过程如下图所示。每个模块的源代码文件(如.c)文件经过编译器编译成目标文件(Objet File,一般扩展名为.o或.obj), 目标文件和库(Library)一起链接形成最终可执行文件。

而最常见的库就是运行时库(Runtime Library),它是支持程序运行的基本函数的集合。库其实是一组目标文件的包,就是一些最常用的代码编译成目标文件后打包存放。

现代的编译和链接过程也并非想象中的那么复杂,它还是一个比较容易理解的概念。比如我们在程序模块main.c中使用另外一个模块func.c中的函数foo()。我们在main.c模块中每一处调用foo的时候都必须确切知道foo这个函数的地址,但是由于每个模块都是单独编译的,在编译器编译main.c的时候它并不知道foo函数的地址,所以它暂时把这些调用foo的指令的目标地址搁置,等待最后链接的时候由链接器去将这些指令的目标地址修正。如果没有链接器,须要我们手工把每个调用foo的指令进行修正,然后填入正确的foo函数地址。当func.c模块被重新编译, foo函数的地址有可能改变时,那么我们在main.c中所有使用到foo的地址的指令将要全部重新调整。这些繁琐的工作将成为程序员的噩梦。使用链接器,你可以直接引用其他模块的函数和全局变量而无须知道它们的地址,因为链接器在链接的时候,会根据你所引用的符号foo, 白动去相应的func.c模块查找foo的地址,然后将main.c模块中所有引用到foo的指令重新修正,让它们的目标地址为真正的foo函数的地址。这就是静态链接的最基本的过程和作用。

3.2动态链接

静态链接这种方法的确很简单,原理上很容易理解,实践上很难实现,在操作系统和硬件不发达的早期,绝大部分系统采用这种方案。

随着计算机软件的发展,这种方法的缺点很快就暴露出来了,那就是静态连接的方式对于计算机内存和磁盘的空间浪费非常严重。特别是多进程操作系统情况下,静态链接极大地浪费了内存空间。

想象一下每个程序内部除了都保留着printf()函数、scanf()函数、strlen()等这样的公用库函数,还有数量相当可观的其他库函数及它们所需要的辅助数据结构。

此外,静态链接对程序的更新、部署和发布也会带来很多麻烦,即一旦程序中有任何模块更新,整个程序就要重新链接、发布给用户。

比如一个程序有20个模块,每个模块1 MB,那么每次更新任何一个模块,用户就得重新获取这个20 MB的程序。如果程序都使用静态链接,那么通过网络来更新程序将会非常不便,因为一旦程序任何位置的一个小改动,都会导致整个程序重新下载。

要解决空间浪费和更新困难这两个问题最简单的办法就是把程序的模块相互分割开来,形成独立的文件,而不再将它们静态地链接在一起。

简单地讲,就是不对那些组成程序的目标文件进行链接,等到程序要运行时才进行链接。也就是说,把链接这个过程推迟到运行时再进行,这就是**动态链接(Dynamic Linking)**的基本思想。同样举个小例子:

还是以Program1和Program2为例,假设我们保留Programl.o、Program2.o和Lib.o三个目标文件。当我们要运行Program1这个程序时,系统首先加载Program1.o,当系统发现Program1.o中用到了Lib.o,即Program1.o依赖于Lib.o,那么系统接着加载Lib.o,如果Program1.0或Lib.o还依赖于其他目标文件, 系统会按照这种方法将它们全部加载至内存。所有需要的目标文件加载完毕之后,如果依赖关系满足,即所有依赖的目标文件都存在于磁盘,系统开始进行链接工作。这个链接工作的原理与静态链接非常相似,包括符号解析、地址重定位等。完成这些步骤之后,系统开始把控制权交给Program1.o的程序入口处,程序开始运行。这时如果我们需要运行Program2,那么系统只需要加载Program2.o,而不需要重新加载Lib.o,因为内存中已经存在了一份Lib.o的副本,系统要做的只是将Program2.0和Lib.o链接起来,不同于静态链接,还需要再次加载Lib.o的副本。

4.静态库与动态库对比

结合上述静态链接与动态链接的对比,可以得到下面静态库与动态库的比较:

***静态库:包含在编译时链接到用户程序的代码,函数和数据都被编译进一个二进制文件(通常扩展名为.LIB)。在使用静态库的情况下,编译链接生成可执行文件时,链接器从库中复制这些函数和数据并把它们和应用程序的其它模块组合起来创建最终的可执行文件(.exe文件)。生成的可执行文件保留了自己的库代码副本。
***动态库(或共享库)包含旨在由多个程序共享的代码,由此动态库也称为共享库。库中的内容在运行时加载到内存中。每个可执行文件不维护其库的复制。使用它的时候往往提供两个文件:一个引入库和一个DLL。引入库包含被DLL导出的函数和变量的符号名,DLL包含实际的函数和数据。在编译链接可执行文件时,只需要链接引入库,DLL中的函数代码和数据并不复制到可执行文件中,在运行的时候,再去加载DLL,访问DLL中导出的函数。

这是使用静态库与动态库的说明。由下图可以看到静态库包含在可执行文件中。而动态库只需要在程序中创建一个符号表(库代码中引用的函数、变量)。

在运行时,动态库在现代操作系统中只被加载到内存中一次,并在依赖它的所有程序之间共享。相反,当使用静态库时,每个可执行文件都必须将库代码加载到内存中。当有多个可执行文件运行时,前者可以提高内存利用率。下图可以解释这种比较。

使用静态库会导致两个明显的缺点:

1. 增加应用程序的大小。如果应用程序包含多个可执行文件,问题会变得更糟。您最终可能会保留同一个库的多个副本。

2. 修改/升级库代码需要重新运行应用程序其他部分的编译/链接。这可能是部署/维护目的的痛苦。大多数时候,一个(非接口相关的)动态库升级不需要重新编译其他部分。

通常,由于上述原因,人们多数时候倾向于选择动态库而不是静态库。然而,动态库并不完美。他们对开发人员有自己的障碍——需要额外关注安装。与生成整体包的静态库不同,动态库必须位于适当的位置以确保可执行文件可以在运行时找到库。

-----静态库在程序编译时会被连接到目标代码中,程序运行时将不再需要该静态库,因此体积较大。
-----动态库在程序编译时并不会被连接到目标代码中,而是在程序运行是才被载入,因此在程序运行时还需要动态库存在,因此代码体积较小。动态库的好处是,不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例。带来好处的同时,也会有问题!如经典的DLL Hell问题。

静态和动态 C++ 库之间的差异10条

序号

比较维度

动态库

静态库

1

build程序

(编译+链接)

编译时:不需要

链接时:需要

编译时:需要

链接时:不需要

(链接发生在构建使用静态库的客户端可执行文件时)

2

二进制文件的本质

没有启动例程的可执行文件。

包含已解析的引用。

目标文件的存档。

所有部分都存在,但大多数引用未解决(本地引用除外)

3

生成可执行文件后需要吗?

需要

动态库需要与可执行文件一起打包,并且必须在可执行文件开始运行时调用(更具体地说,调用动态库提供的函数)

不需要

仅在构建可执行文件在链接阶段)才需要静态库。在运行可执行文件时不再需要,因为库代码已嵌入在应用程序中。

4

磁盘空间效率:磁盘上应用程序之间的代码共享

共享度高

同一个动态库可以在磁盘上的多个可执行文件之间共享。

共享度低

每个可执行文件都需要链接它的静态库的单独副本。这会导致磁盘上出现大量二进制文件副本,尤其是资源受限的移动设备。但是,如果每个应用程序只使用整个静态库的一小部分,磁盘空间效率仍然可以与单个大型 DLL 竞争。

5

内存效率

许多现代操作系统会尝试将动态库代码一次加载到内存中,并在所有需要它的应用程序之间共享。例如,一个 http 网络堆栈可能会在你的日历和笔记本应用程序之间共享。

如果 http 网络堆栈位于静态库中,则每个需要此功能的应用程序都将加载它自己的网络堆栈副本,并通常会影响运行时内存。

6

版本控制问题

可能存在

当应用程序使用的动态库版本与操作系统上存在的旧/新版本库冲突时,你可能会遇到问题。

不存在

由于所有库功能都链接到应用程序中,因此系统上的其他应用程序是否使用不同版本的静态库并没有关系。

7

提供更新/补丁

方便

如果用户希望使用动态库的新(ABI 兼容)版本对应用程序打补丁,他们只需从你那里获取一个新的 dll 并仅修补该 DLL,而无需build整个应用程序。

不太方便

整个应用程序将需要重新构建和修补。这对大型应用程序来说是个大问题,因为现在你需要通过网络提供更大的完整的更新文件。

8

控制加载

在某些系统中,应用程序通过系统调用(如(Windows 上的 LoadLibrary))明确控制何时加载和卸载库功能。这有助于在资源受限的系统上以有效的方式管理应用程序的内存。

不是

当应用程序启动时,整个库被加载到进程空间,并一直保留到应用程序关闭。

9

打包

复杂

在大多数系统中,需要一个单独的步骤来为应用程序创建清单/依赖清单并将其打包。

简单

默认情况下与应用程序/可执行文件本身一起分发 - 无需单独打包。

10

开发过程中的适用性

只有动态库中的功能需要重新编译。

繁琐

整个应用程序将需要重新编译。对于大型应用工具或像 Office 这样大的应用程序,如果所有功能都静态链接而不是在单独的 DLL 中,则可能需要数小时。

5.静态库与动态库的创建与使用

5.1 创建和使用静态库

在这个例子中,我们将创建一个具有一个倒数函数的lib库。库源包含头文件 my_math.h 和源文件 my_math.cpp:


#ifndef H_MY_MATH
#define H_MY_MATH

// my_math.h

double reciprocal(double d);

#endif // H_MY_MATH
#include "my_math.h"
//my_math.cpp

double reciprocal(double d) {
    return 1.0 / d;
}
#include "my_math.h"
#include <iostream>
// main.cpp

int main(){
    std::cout << reciprocal(2.0) << std::endl;
    return 0;
}

头文件 my_math.h 包含在 main.cpp 中,它从库中调用函数:

在第一次编译中,我们将 my_math.cpp 视为一个普通的源文件,一切都按预期的那样:

g++ -c main.cpp -o main.o
g++ -c my_math.cpp -o my_math.o
g++ main.o my_math.o -o a.out
./a.out
0.5

现在让我们将 my_math 打包为静态库。该过程包括 2 个步骤。第 1 步是使用上面相同的命令生成目标文件 my_math.o。第 2 步涉及使用ar(Linux ar chive 实用工具)创建库文件:

ar cr libmy_math.a my_math.o

“cr”标志表示创建一个新的静态库文件。它后面首先是输出文件名的请求名称。注意输出的名称是“libmy_math.a”。在 Linux 中将文件命名为 libXXX.a 作为静态库是一种惯例,请务必这样做。当使用该库时,命令行工具实际上依赖于此约定以使链接器正常工作。

现在我们要使用静态库文件。一种方法是在 g++/gcc 链接命令中将该文件与其他目标文件放在一起。

g++ main.o libmy_math.a -o a.out

另一种更常用的方法是使用 (-L) 和库名称 (-l) 显式指定库路径:

g++ main.o -L. -l my_math -o a.out

这告诉编译器在路径 (.) 中查找名称为 libmy_math.a 的库。注意这里我们使用 -l my_math。链接器会将其视为指定文件名 libmy_math.a(记住我们刚才谈到的创建库的命名约定)。

我们可以通过删除库并运行来验证库是否已被复制到可执行文件中:

rm libmy_math.a 
./a.out
0.5

的确有用。我们刚刚创建了一个静态库并在我们的程序中使用它,验证了静态库的打包及使用。

5.2 创建和使用动态库

这次让我们使用相同的示例代码,而不是创建一个动态库。这是命令:

g++ -shared -o libmy_math.so my_math.o

“-shared”标志指示生成共享库。同样,输出文件命名约定 libXXX.so 是必须的,稍后将由链接器使用。

类似于静态库,我们有两种方式来使用它。1. 将其作为链接器/编译器的输入。2.明确指定库位置(-L)和名称(-l):

g++ main.o my_math.so -o a.out
# or 
g++ main.o -L. -lmy_math -o a.out

简单吧?让我们运行它:

./a.out
./a.out: error while loading shared libraries: libmy_math.so: cannot open shared object file: No such file or directory

糟糕,我们遇到了一个错误(生成了任何一个可执行文件)。运行时试图找到一个名为 libmy_math.so 的共享库,但找不到。发生了什么以及如何解决?

事实证明,用户必须向运行时的可执行文件或操作系统提供提示才能找到共享库 (libmy_math.so)。在 Linux 中有两种方式:

  1. 将共享库路径附加到环境变量 LD_LIBRARY_PATH。

  1. 在构建可执行文件时使用-rpath标志指定共享库路径。

让我们测试一下:

  1. 将库路径添加到 LD_LIBRARY_PATH:

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/cpp_tutorial/static_library
./a.out
0.5

在运行时,OS 会搜索 LD_LIBRARY_PATH 中的每个路径(以“:”分隔)以找到它需要的动态库。通过将路径附加到 LD_LIBRARY_PATH,我们修复了它。

2. 使用-rpath标志:

g++ main.o -L. -lmy_math -o a.out -Wl,-rpath,/home/cpp_tutorial/static_library
./a.out
0.5

这里的“-Wl 标志”意味着它后面的内容(以逗号分隔的标志列表)将被传递给链接器。在这种情况下,“-rpath /home/cpp_tutorial/static_library”被传递给链接器。链接器将此路径信息插入到可执行文件 (a.out) 自己的搜索路径中。这也有效。

比较这两种方法,修改 LD_LIBRARY_PATH 涉及更改影响所有程序的全局变量。使用-rpath通常是首选方式,因为它是本地更改,不会改变其他可执行文件的行为

参考资料

https://blog.csdn.net/qq_41073715/article/details/118516662

https://www.acodersjourney.com/cplusplus-static-vs-dynamic-libraries/

https://domiyanyue.medium.com/c-development-tutorial-4-static-and-dynamic-libraries-7b537656163e

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/337078.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【Vue3】element-plus中el-tree的递归处理赋值回显问题

目录一&#xff1a;先获取所有权限tree二&#xff1a;在获取所有该角色能有的权限tree三&#xff1a;递归处理勾选tree节点由于项目是从0-1开始构建的 rbac都需要重新构建对接 所以涉及到了权限管理和菜单管理 一级菜单包含多个二级菜单 若二级不全选&#xff0c;则一级显示 半…

scipy超几何函数

文章目录hyp2f1广义超几何函数其他超几何函数hyp2f1 当c不是0,−1,⋯0,-1,\cdots0,−1,⋯时&#xff0c;对于∣z∣<1|z|<1∣z∣<1&#xff0c;超几何函数可表示为 2F1(a;b;c;z)∑n0∞a(n)b(n)c(n)znn!_2F_1(a;b;c;z)\sum^\infty_{n0}\frac{a^{(n)}b^{(n)}}{c^{(n)}}\…

TOOM告诉你企业舆情监测的重要性,企业舆情监测的意义

企业舆情监测是一种有效的企业管理手段&#xff0c;能够帮助企业了解舆情信息&#xff0c;从而更好地管理企业、保护企业利益&#xff0c;TOOM告诉你企业舆情监测的重要性&#xff0c;企业舆情监测的意义。 一、企业舆情监测的重要性 声誉管理&#xff1a;通过对企业在线和离…

pixhawk2.4.8使用调试记录—APM固件

目录一、硬件准备二、APM固件、MP地面站下载三、地面站配置1 刷固件2 机架选择3 加速度计校准4 指南针校准5 遥控器校准6 飞行模式7 紧急断电&无头模式8 基础参数设置9 电流计校准10 电调校准11 起飞前检查&#xff08;每一项都非常重要&#xff09;12 飞行经验四、遇到的问…

ESP8266_Linux环境搭建

工具链设置 适用于 Linux 的 ESP8266 工具链可从 Espressif 网站下载&#xff1a; 对于 64 位 Linux&#xff1a; https://dl.espressif.com/dl/xtensa-lx106-elf-gcc8_4_0-esp-2020r3-linux-amd64.tar.gz 对于 32 位 Linux&#xff1a; https://dl.espressif.com/dl/xten…

web自动化使用xpath轴定位

目录 XPath 轴&#xff08;Axes&#xff09; 一、定义&#xff1a;轴可定义相对于当前节点的节点集。 二、语法&#xff1a; 一、ancestor 选取当前节点的所有先辈(父&#xff0c;祖父等) 二、ancestor-or-self&#xff1a; 选取当前节点的所有先辈&#xff08;父、祖父等…

QT(11)- QThread

1 简介 QThread&#xff1a;具有可选事件循环的低级 API QThread是 Qt 中所有线程控制的基础。每个QThread实例表示并控制一个线程。 QThread可以直接实例化&#xff0c;也可以子类化。实例化QThread提供了一个并行事件循环&#xff0c;允许在辅助线程中调用QObject插槽。对 …

Leetcode力扣秋招刷题路-0037

从0开始的秋招刷题路&#xff0c;记录下所刷每道题的题解&#xff0c;帮助自己回顾总结 37. 解数独 编写一个程序&#xff0c;通过填充空格来解决数独问题。 数独的解法需 遵循如下规则&#xff1a; 数字 1-9 在每一行只能出现一次。 数字 1-9 在每一列只能出现一次。 数字…

React hooks之useEffect《类比Vue来记忆》(二)

系列文章目录 下面是正文 文章目录系列文章目录前言一、useEffect的三种形态1.useEffect不传第二个参数代码如下&#xff1a;效果图如下&#xff1a;2.useEffect第二个参数传 []代码如下&#xff1a;效果图如下&#xff1a;3.useEffect第二个参数传 [num]代码如下&#xff1a;效…

java图

1 图基本介 1.1 为什么要有图 前面我们学了线性表和树线性表局限于一个直接前驱和一个直接后继的关系树也只能有一个直接前驱也就是父节点当我们需要表示多对多的关系时&#xff0c; 这里我们就用到了图。 1.2 图的举例说明 图是一种数据结构&#xff0c;其中结点可以具有零…

VL10 使用函数实现数据大小端转换

一、function和task都是为了模块化、结构化设计&#xff0c;主要还是将重复性的功能封装起来方便调用。可以对返回值类型和范围不进行定义&#xff0c;默认值为reg型并且位宽为1变量类型说明 比如integer ifunction(其功能同之前的module模块类似)通常是用来描述组合逻辑&#…

Hi3861编译烧录更快捷

HUAWEI DevEco Device Tool是华为面向智能设备开发者提供的一站式集成开发环境。划重点&#xff0c;DevEco Device Tool 3.1 Beta2又上新技能啦——支持纯Windows环境开发Hi3861&#xff0c;显著提升编译、烧录效率&#xff0c;同时还带来了更多实用的功能及模板&#xff0c;为…

介绍项目前期调研、需求分析阶段的工作

title: 介绍项目前期调研、需求分析阶段的工作 date: 2019-07-07 16:06:00 tags: 需求分析前期调研 categories:架构 立项阶段 所谓立项就是公司内部进行研究、讨论决定要不要做这个事情&#xff0c;通常立项分成两个大类&#xff1a; 项目立项 相对比较简单&#xff0c;需…

欧几里得度量和余弦度量的可取消生物识别方案

欧几里得度量和余弦度量的可取消生物识别方案 便捷的生物识别数据是一把双刃剑&#xff0c;在为生物识别认证系统的繁荣铺平道路的同时&#xff0c;也带来了个人隐私问题。为了缓解这种担忧&#xff0c;提出了各种生物特征模板保护方案来保护生物特征模板免于信息泄露。现有提案…

大道至简 初识springboot

参考文档&#xff1a;springboot官方中文文档 开发工具&#xff1a;IntelliJ IDEA 入门 springboot介绍 Spring Boot帮助你创建可以运行的独立的、基于Spring的生产级应用程序。 我们对Spring平台和第三方库采取了有主见的观点&#xff0c;这样你就能以最少的麻烦开始工作。 …

Service基础使用

Service简介 Service是什么 Service是一个应用组件&#xff0c;它用来在后台完成一个时间跨度比较大的工作&#xff0c;且没有关联任何界面。 Service的生命周期方法在主线程运行。 使用场景 service用于在后台完成用户指定的操作。 访问网络&#xff1b;播放音乐&#xf…

指针的步长及意义(C语言基础)

指针的步长及意义 文章目录指针的步长及意义指针变量1后偏移的字节数不同指针[解引用](https://so.csdn.net/so/search?q解引用&spm1001.2101.3001.7020)时取出的字节数不同其他例子不同类型的指针有何不同的意义指针变量1后跳跃字节数量不同解引用的时候&#xff0c;取出…

虹科方案 | 制药环境中冰箱温度记录的最佳实践——全集成温度监测系统

有效监测冰箱温度是药店、医疗中心和制药实验室的一项重要要求。保持准确的冰箱温度记录对所有储存处方药和疫苗的设施来说是必不可少的&#xff0c;但实现这一目标的最佳方法是什么&#xff1f;● 制药机构需要在特定的温度下储存疫苗和处方药&#xff0c;以保证病人的安全并确…

微信小程序-常用api

文章目录微信小程序-常用api路由wx.switchTab(Object object)wx.navigateTo(Object object)wx.navigateBack(Object object)wx.redirectTo(Object object)提示wx.showToast(Object object)wx.showModal(Object object)wx.showLoading(Object object)wx.showActionSheet(Object …

了解线程池newFixedTheadPool

什么是线程池 操作系统 能够进行运算 调度 的最小单位。线程池是一种多线程处理形式。 为什么引入线程池的概念 解决处理短时间任务时创建和销毁线程代价较大的弊端&#xff0c;可以使用线程池技术。 复用 饭店只有一个服务员和饭店有10个服务员 线程池的种类 newFixedThea…