今天我们就要步入新的内容:关于库的制作与原理,这方面的知识想必一直是大家在学习中的痛点,今天我们就来好好聊聊它的知识。
一.什么是库?在讲解后面的知识前我们先要弄明白到底什么是库?
简而言之,库是写好的现有的,成熟的,可以复⽤的代码。现实中每个程序都要依赖很多基础的底层库,不可能每个⼈的代码都从零开始,因此库的存在意义⾮同寻常。
在我们日常写C语言代码时,所包含的
就拿
所以才有了库的存在,也就有了我们上面说的库是写好的,成熟的,可以复用的代码,本质上来说库是一种可执行代码的二进制形式,可以被操作系统载入内存执行,这句话讲到后面我们就理解了。
库分为两种:静态库和动态库,在不同的操作系统下,它们的后缀也有差异:
linux可能我们用的少,但windows大家都经常使用,而现在我们就知道了在某些文件中的以.lib或者.dll为后缀的文件是什么了。
而我们今天就来讲大家不太熟悉的linux中的动静态库的相关知识。
二.静态库的制作与使用首先我们来讲静态库的相关知识,而下面我们先要知道什么是静态库?
2.1何为静态库?下面我举一个例子来带大家了解何为静态库:
某天,张三和李四的老师布置了一个作业,这个作业包含.c和.h文件,并要求自己写一个test.c的测试案例来检验自己所写的代码,但是呢,张三不会写,就来寻求李四的帮助,想让李四把把源代码发给他,这样张三就能完成作业了。
但是李四说:“ 那万一老师检查源代码怎么办,到时候我俩如果源代码一样不就炸了,我肯定不能把源代码给你啊 ”,所以最终李四并没有将源代码发给张三,但是他也不会一点不帮助张三,所以他的选择是将所有.c的文件编译成.o文件,并将他写的所有.h文件一并发给了张三。
这样,张三就可以通过包含.h头文件,再将自己写的test.c先编译成.o文件,然后和李四发给他的所有.o文件一并链接就可以形成可执行文件了,上面这种方式我们了解过编译和链接过程的应该都能理解。
我们本来就要把所有的.c文件先编译成.o文件,然后将所有的.o文件链接成一个可执行文件,这个.o文件是一个二进制文件,所以即使李四将.o文件发给张三,张三也是看不懂的,并不知道源代码。
那么此时张三手上就有了上面的这些文件,这里大家不必关心这些文件的具体实现是什么,不重要。
那么接下来张三就可以通过上面的这些文件来形成可执行文件了:
这里就按照上面我们所说的方式将所有的.o文件链接成了一个myexe的可执行文件,并成功执行了。而对于张三来说,剩下的就只能听天由命了,只希望老师不会检查他的源代码。
但是,在后面的学习中,老师布置的作业难度越来越大,要建立的文件越来越多,而张三每次都要李四给他发,这一来一回李四觉得每次都发一堆.o给张三,有时候还可能漏了某个.o文件,他就想着能不能将这些.o文件打包到一块,这样每次只需要将这个打包好的文件发给张三就行了,也就是这样:
通过ar命令,我们就形成了一个以.a为后缀的文件,没错,这就是我们要引出的静态库文件!!!
我们打包形成的文件,这个以.a为后缀的文件就叫做静态库文件,本质就是一堆.o目标文件的集合。
这里还有些知识点要讲,我们下面来看:
1.ar命令:这个命令是专门用针对静态库文件进行创建,修改和提取的一个命令,后面所跟的-rc,r就是replace的意思,也就是如果到当前目录下已经有了这个文件,那么就替换它,c就是create的意思,这个相信大家都能理解,没有这个就创建一个就完了。
2.动静态库的命名:这个点我们要注意,在给动静态库文件命名时,前面强烈建议或者强制要求要加上lib,不管是动态库文件还是静态库文件皆是如此。
而命名后,去掉lib前缀,去掉.a的后缀,剩下的才是我们库文件的真正名字,而我们上面的静态库文件的名字就为myc。
2.2静态库的使用上面我们已经创建了出了静态库文件,那这个库文件要如何使用呢?我们下面就来看看:
依旧是上面张三和李四的例子,此时李四将他所制作的静态库文件发给了张三,而此时张三什么也不问就直接开始使用了:
而此时就报出了这样的错误,意思就是找不到你test.c文件中所使用的这些函数,那么原因就是我们的库文件此时并没有生效,出现上面的错误是因为gcc不知道你要使用的是那个库文件,因此我们要在后面指明库名称:
我们在整条命令的后面加上-l+库文件名称,就可以发现上面的找不到库函数的错误已经消失了,取而代之的是找不到我们的库文件。
而在解决第二个问题之前我先把-l+库文件名称这种方式给说了,-l+库文件的名称就是告诉gcc编译器我要用的库文件叫做myc,这样gcc就知道了你要用的库文件是谁!!!
那么回归正题,既然我们已经点明了我们要使用的库文件,为什么还会出现找不到库文件的错误呢?该如何解决呢?我们来看:
先说如何解决,我们在后面加上-L+库文件所在路径即可解决问题,-L+库文件所在路径就是告诉gcc编译器我要用的库文件在当前路径下,所以我用" . "来代替,这样gcc编译器既知道了我们要使用的库文件是谁,又知道了库文件在哪儿,那么就能成功编译了。
下面说为什么要指明库文件所在路径:gcc编译器其实并不会在当前路径下去找你的库文件,而是在系统的默认路径下去找库文件,那么系统的默认路径是哪条路径呢?
在ubuntu系统下,gcc是在/lib/x86_64-linux-gnu的路径中寻找,可以看到在这个路径中有很多的库文件,这还只是展示了一部分。
而在centos系统下,这个路径是:/lib64,名字是要比ununtu短一点,不过我现在用的ubuntu系统,所以centos的就不介绍了,我们主要来看ubuntu系统的。
那么gcc在这个路径中没有找到我们要是用的库文件,所以就会报上面找不到库文件的错误。
并且我们通过ldd命令来查看myexe的依赖库信息发现并没有我们的静态库,这是为什么呢?
答案就是静态库会将自己的代码合并或者说拷贝到可执行程序中,也就是形成的可执行程序中什么都有了,所以一旦形成可执行程序后,就要卸磨杀驴了,不再依赖静态库了。
既然不再依赖静态库,我们通过ldd命令自然也就看不到所依赖的静态库了,也没必要知道所依赖的静态库是谁,因为现在的可执行程序已经什么都有了!!!
那么在有了上面的对静态库使用的基本认识后,相信大家心中还有疑惑,下面我来为大家解决。
2.3解决库文件使用过程中的问题我想大家最大的疑问就是:既然使用库文件时要指明库文件名称和库文件所在路径,为什么我们在使用C语言库时没有加上这些指明条件呢?
这其实是两个问题,分别问的是:使用C语言库时为什么可以不指明库文件是谁?和使用C语言库时为什么可以不指明库文件在哪儿?
我们先来解决第一个问题:要想知道为什么在使用C语言标准库时可以不指明库文件是谁,我先问你一个问题:你猜为什么C语言的编译器叫gcc?
gcc是专门用来编译C语言的,所以gcc默认就要" 认识 "C语言的库!!!
故gcc编译C语言文件时,并不需要指明库文件的名称,因为它本就认识C语言的库,而我们自己实现的库,属于第三方库,gcc不认识,所以要指明库文件是谁。
第二个问题:既然我们说了gcc编译器会到系统默认的路径下去找你要使用的库文件,那么第二个问题其实已经解决了。
没错,就是因为C语言的库文件已经在系统的默认路径下了,所以gcc在编译时就能找到要使用的C语言库文件。
那么此时有人就要问了:也就是说如果把我们自己实现的库文件也放到系统默认的路径下,就能不用-L指明库文件路径了吗?
答案是的,我们来试验一下看看:
答案正如我们所料,当我们将库文件拷贝到系统默认的路径下后,不需要-L来指明库文件所在路径,同样也能编译成功。
2.4静态库使用的延伸知识在这里我们在讲些额外的知识,我们来看:
我们在包含头文件时应该都有过这样两种不同的包含头文件的方式,分别是:" "包含和< >包含,这两种方式的区别可能有人不了解,所以这里简单说明一下。
" "包含的头文件表示编译时在当前目录下寻找头文件,< >包含的头文件表示编译时在系统目录下云找头文件。
因为我们平常写的头文件和源文件都在一个目录下,所以一般用" "来包含头文件,而C语言的头文件和源文件并不在一个目录下,所以要用< >来包含头文件,那我今天就非要用< >来包含我们自己的头文件呢?我们来试一下:
在上面我们就用< >来包含我们自己的头文件,编译后结果显示找不到我们自己的头文件了,原因上面也说了,用< >包含的头文件在编译时gcc会去系统目录下去寻找,我们的头文件并没有在系统目录下,所以当然找不到了。
那么这个系统目录是哪个目录呢?
这个路径就是:/usr/include,可以看到这个路径下包含了一堆头文件,包括我们经常用的stdio.h等等。
所以要解决上面的问题有两种方式:
第一种就是将我们自己写的头文件也添加到系统目录下,但是上面我已经这样操作过了,下面我们换个玩法。
上面就是第二种做法,只需要在后面加上-I+头文件所在目录即可,这样做的含义就是让gcc编译器在指定目录下去寻找要使用的头文件,我们自己的头文件就在当前目录下,所以-I后面跟个" . "即可。
而有了上面的的知识后,我们来总结到底何为库?
库 = 头文件 + 库文件!!!
我们要使用库文件中定义的各种函数,就需要包含相应的头文件,所以一个库严格来说是由头文件和库文件所构成的。
而要使用库就需要搜索:1.头文件(-I(大写i)) 2. 库路径(-L) 3.库是谁(-l(小写L))。
如果我们不想使用过多的选项,就需要将我们自己的库文件和头文件拷贝到相应的系统目录下,但是至少要写-l+库文件名称来指明你要使用的库是谁!!!
因为我们在自己写的库属于第三方库,gcc编译器并不认识。并不只是我们自己写的库,我们在linux中安装其他的库时,本质就是把库文件和头文件拷贝到系统默认的的路径下。
而有了上面对库使用的完整认识后,我们可以尝试把我们自己写的头文件和库文件来形成一个真正的库给别人使用:
代码语言:javascript复制libmyc.a:mystdio.o mymath.o
ar -rc $@ $^
%.o: %.c
gcc -c $<
.PHONY:clean
clean :
rm -rf *.a *.o mywarehouse*
.PHONY:output
output :
mkdir -p mywarehouse/include
mkdir -p mywarehouse/lib
cp *.h mywarehouse/include
cp *.a mywarehouse/lib
tar -czf mywarehouse.tgz mywarehouse通过上面的操作我们就可以形成我们的库文件了,但这还不够,我们接着看:
当我们执行make output的命令后,就可以形成我们自己写的一个完整的库,包括头文件和库文件,并且我们还将库压缩成了一个.tgz为后缀的压缩包,将其发给别人使用,当别人想使用时,将其解压即可。
我们来试验一下,依旧拿上面张三和李四的例子,现在上面的情况是李四已经将自己写的库已经整成了压缩包,准备将压缩包发给张三。
那么现在,李四就将压缩包发给了张三,张三也同时将压缩包进行了解压,此时在张三手中就有了和李四手中一样的mywarehouse库,里面包含库的头文件和库文件。
而张三此时要使用库就可以这样写:
将使用库的三步操作全部写上,张三就能成功的使用李四所提供的库了,如果你觉得后面跟的选项太长,就可以选择将mywarehouse中的头文件和库文件拷贝到系统默认的路径下。
三.动态库的制作与使用在有了上面的对静态库的认识之后,我们下面理解动态库就容易了许多。而动态库和静态库一样,本质都是一堆.o目标文件的集合,所以下面就不再阐述何为动态库了,我们下面直接就从动态库的使用开始。
3.1动态库的使用首先就是在生成.o目标文件时的不同,在动态库中我们在-c的前面加上-fPIC来生成相应的.o目标文件。
而这个fPIC的作用是产生位置无关码(position independent code),这个我们现在只需要对这个东西有个概念即可,这个在后面的动态库加载中会讲到。
下面我们来看将.o文件打包成库文件时的不同:
而与生成静态库文件不同的是:生成静态库文件需要使用ar命令,而生成动态库文件则并不需要,依旧以gcc的方式进行链接,不过要在后面加上-shared!!!
那么这个shared又是什么东西呢?
shared表示生成共享库格式,通过这种方式我们就可以将所有的.o目标文件链接为我们要使用的动态库文件。这个shared这里大家也是有个概念就行,在后面的内容中会讲。
好了,有了上面的前置工作后,我们下面来使用我们的库文件试一下,看是否也与使用静态库文件时有所不同呢?
看来生成可执行文件上并没有什么区别,但是真的只有上面有区别吗?我们来执行一下myexe试试:
看来不同终于出来了,上面的静态库文件执行到这一步我们的myexe可执行文件已经能够执行了,但是在动态库这里却报了上面的错误,这个错误表示动态链接器找不到所需的共享库文件!!!
我们通过ldd命令来查看可执行文件的依赖库信息,发现里面所依赖的动态库文件显示:not found,这就很奇怪了,明明上面我们已经通过-L和-l指明了库文件名称和库文件所在位置,为什么还显示找不到呢?
在解决这个问题之前我们要知道动态链接和静态链接的区别,这两者的区别我在基础开发工具那里用一个形象的例子讲过,如果不清楚,请移步:Linux操作系统-基础开发工具(一)。
当我们形成可执行文件时,静态库会将自己的代码拷贝给可执行文件,所以可执行文件不再依赖静态库了,但动态库不一样,在形成可执行文件时,并不会将自己的代码拷贝给可执行文件,这就导致我们运行可执行文件时依旧需要依赖动态库。
也就是说我们把可执行程序加载到内存中的时候,同时也要把所依赖的动态库文件也加载到内存中,那要讲动态库文件加载到内存中,那得让OS或者加载器知道库文件在哪儿吧。
所以我问一个问题:上面的gcc -o myexe test.c -L. -lmyc到底将信息告诉了谁?
是不是只是将信息告诉了gcc编译器啊,所以命令才能成功,但是我们并没有将信息告诉OS或者加载器,而OS或者加载器也会去系统默认的路径下寻找我们要使用的动态库文件,这个路径和上面是一样的。
而我们的动态库文件不在系统默认的路径下,OS或者加载器自然也就找不到,也就会报上面的错误了。
所以要解决上面的问题,老方法依旧是讲库文件直接拷贝到系统默认的路径下,但我们今天不用这种方式了,下面我将列举三种方式来解决这种问题:
第一种方式:在系统默认的目录下,建立软链接文件
我们可以在系统默认的目录下创建动态库文件的软链接文件,软链接文件的作用我们在上一篇中已经讲过,这里就不再赘述。
需要注意的是因为这次创建的软链接文件和我们的动态库文件并不在同一路径下,所以在描述所链接的文件时要写出完整路径,要指定所链接的文件在何处。
第二种方式:配置LD_LIBRARY_PATH环境变量
我们可以通过配置LD_LIBRARY_PATH环境变量来更新myexe所依赖库的信息,将我们的库文件所在路径写入到这个环境变量中后,我们再次通过ldd命令查看myexe所依赖库的信息,就不再显示not found,而是显示出了具体的内容。
此时我们再次执行myexe程序,发现程序成功的执行了。
但是呢,这种方式只是临时方案,我们重启后环境变量中的内容就会消失了,所以如果想一劳永逸,就要用下面的这种方式。
第三种方式:将动态库的路径,全局有效,更改系统配置文件
在这个目录下的文件后缀都是.conf,而我们要做的就是就是创建一个文件,以.conf为后缀,名字可以随便起,将我们写的动态库文件的绝对路径写入其中即可,下面我们来试试:
通过上面这种方式我们也这次看的就更为直观了,在我们的库文件后面跟的就是库文件的绝对路径,我们来执行一下试试:
可以看到,同样也是可以执行的。
上面总共4种方式,而在实际操作中我更推荐使用前两种,也就是讲库文件拷贝到系统目录下和在系统目录下建立库文件的软链接文件这两种方式。
3.2动态库使用的延伸知识现在我们将静态库文件和动态库文件放在一起,那么gcc是会执行静态链接还是动态链接呢?我们一起来看看:
可以看到在依旧是使用了动态链接,那么我就来输出第一个结论:当动态库和静态库同时存在的时候,gcc/g++默认优先使用动态库,默认进行动态链接!!!
我们接着看其他的情况:
现在我把动态库给删了,只剩下静态库,那么此时我们进行链接后,通过ldd命令可以看到没有了之前所依赖的动态库信息,那么我就来输出第二个结论:一个可执行程序,可能会依赖多个库,,如果我们只提供静态库,即使默认是动态链接,gcc也没招,只能对只提供静态库文件的库,进行静态链接!!!
我们接下来就来看最后一种情况:
这次我把静态库文件给删了,只剩下了动态库文件,但我此时要求gcc使用静态链接,然后就报出了上面的错误,显示找不到这个文件,这是为什么呢?
这就是我要输出的第三个结论:-static表示gcc必须才用静态链接的方案,也就是说静态库文件必须存在,如果不存在,就会报上面找不到库文件的存在的错误!!!
这点我们要和上面的默认使用动态链接的方式区别开来,动态链接不强制要求你有动态库文件,没有就用静态库文件,进行静态链接。但是如果你强制要求gcc采用静态链接的方案,那么你就必须有静态库的文件,并不会因为你只有动态库文件而采用动态链接!!!
以上就是庖丁解牛:深入拆解Linux静态库与动态库——从制作到使用的核心技巧讲解的全部内容。