静态库和动态库是我们都非常熟悉的概念,它们的产生根本目的就是为了代码重用。我们平常在工作中也会使用各种库,有静态库,动态库。虽然它们的目的都一致的,但是两种形式的库还是有本质上的区别,静态库是一种比较原始,简单的代码复用方式,而动态库相对就复杂些。本篇是介绍静态库的基本概念和使用细节。
目标文件的复用
静态库的目标就是代码复用,其实目标文件就可以直接提供给其它程序使用。
程序模块划分成多个不同源文件,在程序源文件编译成目标文件后,我们可以将目标文件直接链接到其它程序中。整个过程如下所示:
项目一的目标文件提供给项目二使用,具体的是通过链接器直接将这些目标文件链接项目二的可执行文件中,这就是代码复用,也体现了编译与链接过程分离的意义。
静态库
静态库就是一些目标文件的集合。
我们直接将目标文件打包生成静态库,方便使用。这个过程是可逆的,可以将一个静态库解包成目标文件。
提供头文件和静态库达到代码复用的目的,如下图:
通过上图可以看到构建静态库是没链接的过程的。
按照Linux平台的惯例,静态库文件名以lib开头,文件扩展名为.a。
Linux下使用静态库
作为示例,编写了两个c文件及它们的头文件,分别叫做add.h/.c
int add(int a,int b);
int add(int a,int b) {
return a + b;
}
sub.h/.c
int sub(int a,int b);
int sub(int a,int b) {
return a - b;
}
生成静态库
- 编译生成
add.o
和sub.o
,两个目标文件。
gcc -c add.c sub.c
- 通过
ar
工具将目标文件打包成静态库文件。
ar rcs libtest.a add.o sub.o
将add.o
和sub.o
打包成静态库文件libtest.a
。
ar
工具还可以完成以下任务:
- 从库文件中删除一个或多个目标文件。
- 从库文件中替换一个或多个目标文件。
- 从库文件中提取一个或多个目标文件。
我们看一看libtest.a
包含的符号
nm libtest.a
add.o:
0000000000000000 T add
sub.o:
0000000000000000 T sub
静态库中包含的符号就是 add
和sub
。
链接静态库
编写main文件
#include "add.h"
#include "sub.h"
int main() {
add(10,8);
sub(10,8);
}
链接静态库libtest.a
gcc main.c -L. -ltest -o testlib
生成可执行文件testlib
,其中-L
参数是指定库的路径,静态库是在当前目录下,所以是-L.
。-l
是指定库的名字,-ltest
就是链接静态库libtest.a
,只需指定test
名字即可(编译器会按约定的惯例找libtest.a
库),-o
指定生成的可执行文件的名字testlib
。
我们看看testlib
中的符号是否包含静态库中的符号。
nm testlib | grep add
0000000000400511 T add
nm testlib | grep sub
0000000000400525 T sub
显然是已经包括了静态库中符号add
和sub
。
静态库的使用
静态库将在链接时使用,链接完成后,静态库的节将与客户二进制文件中原有的目标文件节进行无缝链接。
静态库中的符号成为客户二进制文件符号列表中的一部分(见上面的例子),并且保留了可见性,所以使用静态库的程序都比使用动态库的方式,生成的可执行文件会大。
只链接了静态库的程序最终只会有一个可执行程序文件,它可以独立运行,不依赖任何其他库。
Linux下链接静态库遵循的规则:
- 依次链接静态库,每次一个静态库。
- 链接静态库从传递给链接器的静态库列表的最后一个静态开始 (通过命令或makefile),且会反方向逐个链接,直达列表宏的第一个位置。
- 链接器会对静态库进行详细的检索,在所有的目标文件中,只有包含客户二进制文件实际所需符号的目标文件,才会进行检索。
我们有时需要在传递给链接器的静态库多次添加同一个静态库。当一个静态库同时提供了多种完全不同的功能时,就会遇到这种多次添加的情况。
静态库的使用建议
前面提到静态库中所有的符号都成为客户程序符号的一部分。所以如果静态库中某个方法或变量变动,意味着整个客户程序都需要重新链接,不管客户程序是否有改动(即使只是实现方式的变动,而不是方法名字,签名参数的变动)。这也是使用静态库最大局限性,无法动态更新静态库,而不影响客户程序。
所以对应用静态库一般是有如下建议:
- 比较核心的算法,基础功能可以通过静态库的方式提供,因为这些核心,基础的代码一般不会轻易改动。
- 如果要求是单文件部署。