库
什么是库文件
说起库文件,我们首先得先来了解下单一模型和分离模型这两个概念:
-
单一模型
—在程序中,我们如果将各种函数等实现的功能如果都放在一个单一的源文件内部,我们把这种编程模型叫做单一模型。
—这种模型的缺点是:编译的时间长,不易于维护和升级,而且不易于协作开发。
-
分离模型
—跟单一模型相反,分离模型就是将不同的功能函数都放在不同的源文件中。
—这样做的优点是:极大的缩短了编译时间,易于维护和升级,易于协作开发。
—但同时分离模型带来的缺点是:如果目标文件太多,那我们管理起来就会显得很麻烦。
有没有一种方法,能把所有的目标文件都放在一个仓库里,我们要用目标文件的功能时直接上仓库中去找就行…
于是就有了库,它能将所有的目标文件统一整理成为一个文件,便于使用和管理。
库文件有两种,一种是静态库,另一种是动态库。
总结:什么是库文件
-
为何要把一个程序分成多个源文件,并由每个源文件编译生成独立的目标文件?
------ 化整为零,易于维护,便于协作。
-
为何要把多个目标文件合并成一个库文件?
------ 集零为整,方便使用,易于复用。
-
可以把简单的库文件看成一种代码仓库,它提供给使用者一些可以直接拿来用的变量,函数和类
------ 库文件一般指计算机上的一类文件,分两种,一种是静态库,另一种是动态库
静态库
静态库的本质就是将多个目标文件打包成一个文件
链接静态库的本质就是将库中被调用的代码复制到调用模块中去
静态库的扩展名是: .a 例如: libxxx.a
静态库的制作和使用
以构建一个简单的数学库为例,构建静态库的顺序如下:
- 编译库的实现代码和接口声明
计算模块:calc.c calc.h
显示模块:show.c show.h
接口文件:math.h
- 编译成目标文件
gcc -c calc.c —> calc.o
gcc -c show.c —> show.o
- 打包成静态库
ar -r libmath.a calc.o show.o
上面是大概过程,接下来我们用代码演示
PS:计算模块实际就是实现加减运算,显示模块就是显示加减等式。这里仅仅用来演示静态库的制作!
首先我们先来写calc.h,也就是计算模块的头文件
//计算模块头文件
#ifndef __CALC_H
#define __CALC_H
//加法函数
int add( int, int );
//减法函数
int sub( int, int );
#endif //__CALC_H
下面是写calc.c,也就是计算模块的实现
//计算模块实现
#include "calc.h"
//加法函数
int add( int x, int y ){
return x + y;
}
//减法函数
int sub( int x, int y ){
return x - y;
}
接下来是写show.h,也就是显示模块的头文件
//显示模块头文件
#ifndef __SHOW_H
#define __SHOW_H
//显示模块函数
void show( int, char, int, int );
#endif //__SHOW_H
接下来是show.c, 也就是显示模块的实现
//显示模块实现
#include "show.h"
#include <stdio.h>
void show( int a, char op, int b, int res ){
printf("%d %c %d = %d\n", a, op, b, res);
}
最后是接口文件math.h的制作
//接口文件
#include "calc.h"
#include "show.h"
**为什么要接口文件呢?**因为我们调用模块main.c文件中如果调用这些函数的话,就得包含calc.h, show.h这两个头文件。
如果我们的目标文件很多的话,那么都要添加到调用模块main.c中,这无疑让main.c显得很臃肿。
如果我们把所有的头文件都加到接口文件中,那么调用模块中我们只要添加接口文件就行,减少了调用模块main.c的代码量
好啦,到这里我们就完成了编译库的实现代码和接口声明
接下来就是生成目标文件 calc.o show.o 文件
gcc -c calc.c //生成calc.o目标文件
gcc -c show.c //生成show.o目标文件
生成目标文件后,接下来我们就可以打包我们的静态库啦!
打包静态库,我们用ar -r libmath.a calc.o show.o 命令
ar -r libmath.a calc.o show.o
—可以看到我们目录下就生成了一个 libmath.a 静态库文件
—这里补充一个 ar 命令知识
ar命令
ar [ 选项 ] <静态库文件> <目标文件列表>
-r 将目标插入到静态库,已存在则更新
-q 将目标文件追加到静态库尾部
-d 从静态库删除目标文件
-t 列表显示静态库中的目标文件
-x 将静态库展开为目标文件
—好的,那我们现在已经做好了静态库,接下来我们就是链接并调用静态库
—我们用 main.c 来调用库中的功能,并且链接 main.c 和我们的静态库 libmath.a
首先,我们先在 main.c 写我们的调用模块
//调用模块
#include <stdio.h>
/*#include "calc.h"
#include "show.h"*/
#include "math.h"
int main(void){
int a = 3;
int b = 5;
int res = add(3, 5); //计算a + b
show(a, '+', b, res); //显示a + b
show(a, '-', b, sub(a, b)); //显示a - b
return 0;
}
写完了调用模块后,接下来就是将我们的调用模块和我们的静态库链接
- 因为我们的调用模块和我们的静态库文件在同一个目录文件下,所以我们可以直接编译链接就行
gcc main.c libmath.a
1.但是呢?我们也会遇到如果我们的调用模块和我们的库并不在同一个路径下的情况,这时我们就不能直接编译链接。
2.我们需要手动添加,一个库的指定路径,这个路径可以是相对路径也可以是绝对路径。
3.这样编译器就可以帮我们找到静态库,并且和我们的调用模块链接
2.如果我们的静态库和我们的调用模块不在同一路径,我们得用 -l 指定库名, 用 -L 指定库的路径。
/*格式:
gcc main.c -l库名 -L库路径
*/
//比如这里我的静态库如果在我的上一级目录,那么我应该这样编译链接
gcc main.c -lmath -L..
3.除了上面这种方法,指定路径以外。我们还可以用 -l 指定库名,手动添加一个环境变量来告诉编译器库的路径
这个环境变量是:LIBRARY_PATH
//在终端窗口下敲这些,这些还是假设库在我的上一级目录下,其他目录一样用相对路径或者绝对路径添加即可
LIBRARY_PATH=$LIBRARY_PATH:..
export LIBRARY_PATH
当然如果你重新开一个终端窗口就得手动添加该环境变量,想一劳永逸的话就在 .bashrc 脚本文件下添加该环境变量。
关于 .bashrc 文件的相关知识在我的以前文章中,这里就不一一赘述了。
—好的,我们现在通过LIBRARY_PATH环境变量已经指定库的路径了,接下来,我们在重新用 -l库名 编译链接试试
gcc main.c -lmath
//编译完美通过
那关于静态库的知识我们就讲完啦,还有不懂得可以私信我偶!
接下来是动态库的制作和使用。
动态库
1.动态库和静态库不同,链接动态库不需要将被调用的函数代码复制到包含调用代码的可执行文件中。相反链接器会在****调用语句嵌入一段指令,在程序执行到这段指令时,会加载动态库并寻找被调用函数的入口地址并执行之。
2.简单来说,就是动态库并不是将库中的代码全部复制一份到调用代码的可执行文件中,而是在调用代码处创建一个调用函****数的入口地址,通过这个地址可以直接找到动态库,并执行调用函数。
3.如果动态库中的代码同时被多个进程所用,动态库在内存的实例仅需一份,为所有使用该库的进程所共享,因此动态库也叫做共享库。
4.动态库的扩展名是: .so 例如: libxxx.so
动态库的制作和使用
以构建数学库为例,动态库的构建顺序如下:
- 编译库的实现代码和接口声明
计算模块:calc.c calc.h
显示模块:show.c show.h
接口文件:math.h
- 编译成目标文件
gcc -c -fpic calc.c —> calc.o
gcc -c -fpic show.c —> show.o
- 打包成动态库
gcc -shared calc.o show.o -o libmath.so
动态库的制作跟静态库大致差不多,唯一就是编译目标文件要加 -fpic 指令,还有打包动态库时不一样
//编译链接也可以一步完成
gcc -shared -fpic calc.o show.o -o libmath.so
接下来,我们动态库和调用模块的链接方法是一样的
1.如果我们调用模块和动态库在同一路径,我们直接链接就行
2.如果我们动态库和我们的调用模块不在同一个路径下,那么我们可以通过 -l 指定库名 -L 指定路径来编译链接
3.或者我们也可以 -l 指定库名,然后手动添加环境变量LIBRARY_PATH来告诉编译器库的路径
—然后,我们编译链接后,生成了可执行文件 a.out 后,我们运行
—结果,终端窗口弹出来这样一句话:
大致意思就是编译过程没问题,但是在执行过程中,执行到调用函数代码处找不到跳转入口,也就是无法跳转到我们的动态库里面。
这也是跟我们静态库最大的不同点。
实际上这是我们的链接器不知道我们的动态库在哪里,这是我们任需要手动添加一个环境变量LD_LIBRARY_PATH里面添加动态库的路径,来告诉我们的链接器我们动态库在哪里。
具体代码实现如下,这里假设我的动态库在我的上一级目录
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:..
同样,我们如果一劳永逸还是添加到 .bashrc 脚本文件中合适。
到这里,我们在运行一下 a.out 完美运行!
那么,我们就大功告成啦!
**//在生成可执行程序的链接阶段,将不讲所调用函数的二进制代码复制到可执行程序中
//只是将该函数在共享库中的地址嵌入到调用模块,因此运行时需要依赖动态库**
PIC(Position Independent Code, 位置无关码)
—调用代码通过相对地址标识调用代码的位置,模块中的指令与该模块被加载到内存中的位置无关
**> -fPic:大模式, 生成代码比较大, 运行速度比较慢, 所有平台都支持
-fpic:小模式, 生成代码比较小, 运行速度比较快, 仅部分平台支持**
------这里我们不必纠结,简单了解下就好。