静态链接库和动态连接库
库文件,读者可以将其等价为压缩包文件,该文件内部通常包含不止一个目标文件(也就是二进制文件)。值得一提的是,库文件中每个目标文件存储的代码,并非完整的程序,而是一个个实用的功能模块。
库文件的产生,极大的提高了程序员的开发效率,因为很多功能根本不需要从 0 开发,直接调取包含该功能的库文件即可。并且,库文件的调用方法也很简单,以 C 语言中的 printf() 输出函数为例,程序中只需引入 <stdio.h> 头文件,即可调用 printf() 函数。调用库文件为什么还要牵扯到头文件呢?首先,头文件和库文件并不是一码事,它们最大的区别在于:头文件只存储变量、函数或者类等这些功能模块的声明部分,库文件才负责存储各模块具体的实现部分。读者可以这样理解:所有的库文件都提供有相应的头文件作为调用它的接口。也就是说,库文件是无法直接使用的,只能通过头文件间接调用。
编译器提供有 2 种实现链接的方式,分别称为静态链接方式和动态链接方式,其中采用静态链接方式实现链接操作的库文件,称为静态链接库;采用动态链接方式实现链接操作的库文件,称为动态链接库。
静态链接库
静态链接库实现链接操作的方式很简单,即程序文件中哪里用到了库文件中的功能模块,GCC 编译器就会将该模板代码直接复制到程序文件的适当位置,最终生成可执行文件。
使用静态库文件实现程序的链接操作,既有优势也有劣势:
- 优势是,生成的可执行文件不再需要任何静态库文件的支持就可以独立运行(可移植性强);
- 劣势是,如果程序文件中多次调用库中的同一功能模块,则该模块代码势必就会被复制多次,生成的可执行文件中会包含多段完全相同的代码,造成代码的冗余。
和使用动态链接库生成的可执行文件相比,静态链接库生成的可执行文件的体积更大。在 Linux 发行版系统中,静态链接库文件的后缀名通常用 .a 表示;在 Windows 系统中,静态链接库文件的后缀名为 .lib。
动态链接库
动态链接库,又称为共享链接库。和静态链接库不同,采用动态链接库实现链接操作时,程序文件中哪里需要库文件的功能模块,GCC 编译器不会直接将该功能模块的代码拷贝到文件中,而是将功能模块的位置信息记录到文件中,直接生成可执行文件。
显然,这样生成的可执行文件是无法独立运行的。采用动态链接库生成的可执行文件运行时,GCC 编译器会将对应的动态链接库一同加载在内存中,由于可执行文件中事先记录了所需功能模块的位置信息,所以在现有动态链接库的支持下,也可以成功运行。
采用动态链接库实现程序的连接操作,其优势和劣势恰好和静态链接库相反:
- 优势是,由于可执行文件中记录的是功能模块的地址,真正的实现代码会在程序运行时被载入内存,这意味着,即便功能模块被调用多次,使用的都是同一份实现代码(这也是将动态链接库称为共享链接库的原因)。
- 劣势是,此方式生成的可执行文件无法独立运行,必须借助相应的库文件(可移植性差)。
和使用静态链接库生成的可执行文件相比,动态链接库生成的可执行文件的体积更小,因为其内部不会被复制一堆冗余的代码。在 Linux 发行版系统中,动态链接库的后缀名通常用 .so 表示;在 Windows 系统中,动态链接库的后缀名为 .dll。
注意:
GCC 编译器生成可执行文件时,默认情况下会优先使用动态链接库实现链接操作,除非当前系统环境中没有程序文件所需要的动态链接库,GCC 编译器才会选择相应的静态链接库。如果两种都没有(或者 GCC 编译器未找到),则链接失败。
静态链接库的创建和使用
这里以一个例子来演示静态库的创建和使用。该项目中包含 1 个头文件( .h ),4 个源文件( .c ),它们各自包含的代码如下所示: 其中 add.c、sub.c 和 div.c 这 3 个文件中各包含一个函数,分别实现将两个整数做相加、相减和除法操作,而 test.h 仅包含这 3 个函数的声明部分,main.c 是主程序文件,其通过引入 test.h 头文件调用了 3 个函数,从而分别完成了对用户输入的 2 个整数做相加、相减以及除法操作。
[root@bogon demo]# ls <- demo 目录结构 add.c div.c main.c sub.c test.h [root@bogon demo]# cat test.h <- test.h 文件内容 #ifndef __TEST_H_ #define __TEST_H_ int add(int a,int b); int sub(int a,int b); int div(int a,int b); #endif [root@bogon demo]# cat add.c <- add.c 文件内容 #include "test.h" int add(int a,int b) { return a + b; } [root@bogon demo]# cat sub.c <- sub.c 文件内容 #include "test.h" int sub(int a,int b) { return a - b; } [root@bogon demo]# cat div.c <- div.c 文件内容 #include "test.h" int div(int a,int b) { return a / b; } [root@bogon demo]# cat main.c <- main.c 文件内容 #include <stdio.h> #include "test.h" //必须引入头文件 int main(void) { int m, n; printf("Input two numbers: "); scanf("%d %d", &m, &n); printf("%d+%d=%d ", m, n, add(m, n)); printf("%d-%d=%d ", m, n, sub(m, n)); printf("%d÷%d=%d ", m, n, div(m, n)); return 0; }
对于编译、运行该项目,我们可以直接使用 gcc 命令完成:
1、静态链接库的创建
静态链接库其实就相当于压缩包,其内部可以包含多个源文件。但需要注意的是,并非任何一个源文件都可以被加工成静态链接库,其至少需要满足以下 2 个条件:
- 源文件中只提供可以重复使用的代码,例如函数、设计好的类等,不能包含 main 主函数;
- 源文件在实现具备模块功能的同时,还要提供访问它的接口,也就是包含各个功能模块声明部分的头文件。
显然对于 demo 项目中的 add.c、sub.c 以及 div.c 这 3 个源文件来说,以上 2 个条件都符合,因此都可以被加工成静态链接库。并且根据实际需要,我们可以将它们集体压缩到一个静态链接库中,也可以各自压缩成一个静态链接库。
将源文件打包为静态链接库的过程很简单,只需经历以下 2 个步骤:
1) 将所有指定的源文件,都编译成相应的目标文件:
2) 然后使用 ar 压缩指令,将生成的目标文件打包成静态链接库,其基本格式如下:
ar rcs 静态链接库名称 目标文件1 目标文件2 ...
有关 ar 打包压缩指令,以及 rcs 各选项的含义和功能,感兴趣的读者可自行查找相关资料了解。这里需要重点说明的是,静态链接库的不能随意起名,需遵循如下的命名规则:
libxxx.a Linux 系统下,静态链接库的后缀名为 .a;Windows 系统下,静态链接库的后缀名为 .lib。
其中,xxx 代指我们为该库起的名字,比如 Linux 系统自带的一些静态链接库名称为 libc.a、libgcc.a、libm.a,它们的名称分别为 c、gcc 和 m。
下面,我们尝试将 add.o、sub.o 和 div.o 打包到一个静态链接库中:
其中,libmymath.a 就是 add.o、sub.o 和 div.o 一起打包生成的静态链接库,mymath 是我们自定义的库名。
2、静态链接库的使用
静态链接库的使用很简单,就是在程序的链接阶段,将静态链接库和其他目标文件一起执行链接操作,从而生成可执行文件。
以该项目为例,首先我们将 main.c 文件编译为目标文件,然后通过 --static 选项强制 GCC 编译器使用静态链接库
注意:
使用gcc -static命令进行链接时出现以下错误:
/usr/bin/ld: 找不到 -lc
collect2: 错误:ld 返回 1
原因:
在新版本的linux 系统下安装 glibc-devel、glibc和gcc-c++时,都不会安装libc.a. 只安装libc.so. 所以当使用-static时,libc.a不能使用。只能报找不到libc了。
解决方法:
安装 glibc-static
yum install glibc-static
如果 GCC 编译器提示无法找到 libmymath.a,还可以使用如下方式完成链接操作:
其中,-L(大写的 L)选项用于向 GCC 编译器指明静态链接库的存储位置; -l(小写的 L)选项用于指明所需静态链接库的名称,注意这里的名称指的是 xxx 部分,且建议将 -l 和 xxx 直接连用(即 -lxxx),中间不需有空格
动态链接库的创建和使用
在介绍动态链接库时,仍然使用上文中的项目来演示。
动态链接库的创建
总的来说,动态链接库的创建方式有 2 种。
1) 直接使用源文件创建动态链接库,采用 gcc 命令实现的基本格式如下:
gcc -fpic -shared 源文件名... -o 动态链接库名
其中,-shared 选项用于生成动态链接库;-fpic(还可写成 -fPIC)选项的功能是,令 GCC 编译器生成动态链接库(多个目标文件的压缩包)时,表示各目标文件中函数、类等功能模块的地址使用相对地址,而非绝对地址。这样,无论将来链接库被加载到内存的什么位置,都可以正常使用。
由该项目中的 add.c、sub.c 和 div.c 这 3 个源文件生成一个动态链接库,执行命令为:
注意,动态链接库的命令规则和静态链接库完全相同,只不过在 Linux 发行版系统中,其后缀名用 .so 表示;Windows 系统中,后缀名为 .dll。
2) 先使用 gcc -c 指令将指定源文件编译为目标文件。仍以 demo 项目中的 add.c、sub.c 和 div.c 为例,先执行如下命令:
为了后续生成动态链接库并能正常使用,将源文件编译为目标文件时,也需要使用 -fpic 选项。在此基础上,接下来利用上一步生成的目标文件,生成动态链接库:
以上 2 种操作,生成的动态链接库是完全一样的,任选一种即可。
动态链接库的使用
动态链接库的使用场景就是和项目中其它源文件或目标文件一起参与链接。以 demo 项目为例,前面我们将 add.c、sub.c 和 div.c 打包到了 libmymath.so 动态链接库中,此时该项目中仅剩 main.c 源程序文件,因此执行 demo 项目也就演变成了将 main.c 和 libmymath.so 进行链接,进而生成可执行文件。test.h 头文件并不直接参与编译,因为在程序的预处理阶段,已经对项目中需要用到的头文件做了处理。
生成的 main.exe 通常无法直接执行,例如:
可以看到,执行过程中无法找到 libmymath.so 动态链接库。通过执行ldd main.exe
指令,可以查看当前文件在执行时需要用到的所有动态链接库,以及各个库文件的存储位置:
可以看到,main.exe 文件的执行需要 4 个动态链接库的支持,其中就包括 libmymath.so,但该文件无法找到,因此 main.exe 执行会失败。
运行由动态链接库生成的可执行文件时,必须确保程序在运行时可以找到这个动态链接库。常用的解决方案有如下几种:
- 将链接库文件移动到标准库目录下(例如 /usr/lib、/usr/lib64、/lib、/lib64);
- 在终端输入
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:xxx
,其中 xxx 为动态链接库文件的绝对存储路径(此方式仅在当前终端有效,关闭终端后无效); - 修改~/.bashrc 或~/.bash_profile 文件,即在文件最后一行添加
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:xxx
(xxx 为动态库文件的绝对存储路径)。保存之后,执行source .bashrc
指令(此方式仅对当前登陆用户有效)