一. 什么是动、静态库?
1、库
我们在编写一个程序的时候,经常会遇到好多重复或常用的部分,如果每次都重新编写固然是可以的,不过那样会大大降低工作效率,并且影响代码的可读性,更不利于后期的代码维护。我们可以把他们制作成相应的功能函数,使用时直接调用就会很方便,还可以进行后期的功能升级。
库通俗的说就是把这些常用函数的目标文件打包在一起,即库的本质为一堆.o文件的集合。不包含main但是包含了大量的方法,提供相应函数的接口,便于程序员使用。库是别人写好的现有的,成熟的,可以复用的代码,我们只需要知道其接口如何定义,便可以自如使用。比如我们常使用的printf函数,就是 C 标准库提供的函数。我们在使用时只需要包含相应的头文件就可以使用,在连接阶段会连接到具体printf的方法上去,而不用关心printf函数具体是如何实现的,这样就大大提高了程序员编写代码的效率。
从连接方式上库大体上可以分为两类:静态库和动态库。
- 在windows中静态库是以 .lib为后缀的文件,动态库是以 .dll为后缀的文件。
- 在linux中静态库是以 .a为后缀的文件,动态库是以 .so为后缀的文件。
PS:下面我们讨论的都是Linux的库。
库的连接方式
一个程序从源文件编译生成可执行文件的步骤:
其中链接将二进制文件链接成一个可执行的命令,主要是把分散的数据和代码收集并合成一个单一的可加载并可执行的的文件。发生在代码静态编译、程序被加载时的连接称为静态连接,对应连接的库叫做静态库;发生在程序执行时的连接称为动态连接,对应连接的库称为动态库。
2、动、静态库
静态库(.a):程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库。
动态库(.so):程序在运行的时候才去链接动态库的代码,多个程序可以共享使用库的代码。
3、库相关的两个命令
file命令
file命令用来识别文件类型,也可用来辨别一些文件的编码格式。这里我们可以通过file命令查看可执行程序的连接方式。
默认情况下库的连接方式都是动态链接,如果需要静态连接我们需要在编译时加上“-static”选项。
对比同一个源文件静态连接生成的可执行程序大小差不多是动态连接的20倍,因为在静态链接时程序把库的代码链接到可执行程序中了。
ldd命令
全称list dynamic dependence,该命令可以查看一个可执行程序依赖的动态库。
4. Linux中库的标准命名
静态库的名字一般为libxxx.a,其中xxx是该lib的名称。
动态库的名字一般为libxxx.so.major.minor,xxx是该lib的名称,major是主版本号, minor是副版本号。
二. 动、静态库的优缺点
动态库
优点
- 节省内存空间:运行时除了代码加载到内存,其所使用的动态库也会被加到内存,如果其他进程也需要使用该动态库,则只要调整其他进程的映射关系到已经在内存加载好的库即可,不需要重复加载,节省内存空间。
- 将一些程序升级变得简单,用户只需要更新动态库即可。
缺点:依赖库,如果可执行程序生成后,删除库则无法运行这个可执行程序。
动态库
优点:可执行程序的运行与库无关,因为在编译连接时库已经链接到可执行程序中,删除库后仍可运行。
缺点
- 多个进程使用同一库会导致内存资源浪费。
- 静态库对程序的更新、部署和发布页会带来麻烦。如果静态库更新了,所有使用它的应用程序都需要重新编译、发布给用户(对于玩家来说,可能是一个很小的改动,却导致整个程序重新下载,全量更新)。
三. 动、静态库的实现
打包自己的库,这样我们就可以把我们自己写的函数给其他人使用了,比如我们写了一个加法函数如下:把该函数的声明放到头文件里,实现放到一个源文件里。
我们最终想要实现的是只要包含了这个库的头文件就可以使用这个库的接口函数。
1、静态库实现
第一步:汇编生成目标文件(g++ -c +源文件 -o +生成的目标文件名称)
对写有函数实现的源文件使用g++配合-c选项汇编生成相应的目标文件:
第二步:使用ar命令打包静态库(ar -rc +静态库名称 +所有依赖的.o文件)
ar可以集合许多文件,成为单一的备存文件。在备存文件中,所有成员文件皆保有原来的属性与权限。
- 选项里的r表示将文件插入备存文件中的意思,c表示建立备存文件。
- 注意顺序必须是静态库名称在前,目标文件的名称在后。
- 可以有多个目标文件。
打包好了库之后,我们把库和头文件封装到一个目录里,并删除一些其他不用的文件:
第三步:生成静态可执行程序(g++ +源文件名称 -I +头文件路径 -L +库路径 -l +要链接的库名 -o +生成的可执行程序名称)
这里指明库、头文件路径和库名称是要告诉编译器和连接器,保证在它们在工作时能够找到相应的库的数据。另外这里的库名称是指省略前缀lib和后缀.a之后的真正库的名称。
还有一种方法可以生成可执行程序时不加库路径和头文件路径,那就是直接把库和头文件拷贝到系统默认查找路径下,系统会直接到这个默认路径下查找相应文件,但不建议这样做因为这会污染路径源。
头文件的系统默认查找路径:/usr/include
库文件的系统默认查找路径:/lib64
之后在生成可执行程序时不需要指明库路径和头文件路径,但还是要指明需要连接的库的名称。
2、动态库实现
第一步:汇编生成目标文件(g++ -c -fPIC +源文件)
注意在生成.o文件时要生成位置无关码,要在g++后加 -fPIC表示生成位置无关码,这样的代码无绝对跳转,跳转都为相对跳转。
fPIC 的全称是 Position Independent Code,加了 fPIC 实现真正意义上的多个进程共享 .so 文件,当多个进程引用同一个 PIC 动态库时,可以共用内存。这一个库在不同进程中的虚拟地址不同,但操作系统会把它们映射到同一块物理内存上。
如果不加 fPIC,则加载 .so 文件时,需要对代码段引用的数据对象需要重定位,重定位会修改代码段的内容,根据写实拷贝的规则,这就造成每个使用这个 .so 文件代码段的进程在内核里都会生成这个 .so 文件代码段的 copy。每个 copy 都不一样,取决于这个 .so 文件代码段和数据段内存映射的位置。
第二步:使用g++打包动态库(g++ -share +所有依赖的.o文件 -o +动态库标准名称)
-shared选项表示创建一个动态链接库。区分这里打包库的方式是使用g++,而打包静态库的方式是使用ar命令。
整理刚刚生成的库文件和库的头文件并删除一些不用的文件:
第三步:生成动态可执行程序(g++ +源文件名称 -I +头文件路径 -L +库路径 -l +要链接的库名 -o +生成的可执行程序名称)
这一步的操作方法和生成静态可执行程序一样:指明头文件,库路径和库名称。
但是生成的这个可执行程序是不能运行的,虽然它通过了编译器的编译器的语法检查,但是在运行的过程中,操作系统无法查找到可执行程序所依赖的库。
用ldd命令查看该可执行程序的动态依赖关系发现我们自己的动态库没有被找到。
第四步:解决动态库生成的可执行程序无法运行的问题
①将依赖的库拷贝到系统路径下
之后操作系统在运行的时候能够在/lib64中找到其依赖的库的路径,不会这样做会污染路径源。
②更改环境变量(LD_LIBRARY_PATH)
把库的绝对路径导入到环境变量LD_LIBRARY_PATH中:
这样操作系统就可以通过LD_LIBRARY_PATH找到库的路径,不过这样导入的环境变量只是临时的,下次登录后会被清除。
③更新ldconfig配置
将自己库的路径写到配置文件/etc/ld.so.conf里
再使用命令ldconfig刷新动态库,之后操作系统就会在/etc/ld.so.conf里找到对应配置文件里保存的库的路径里。