为什么会有库
在我们实际开发中,一定会遇到协同开发,或者直接使用别人的代码的情况。其实就是用别人的代码以提升我们的效率。实际上就是别人把代码功能写好了,我们再基于别人的代码做二次开发,如何在c/c++中使用别人的功能,这时就需要我们的库。
动静态库的基本原理
我们知道,源文件和头文件编译生成一个可执行程序需要经历4个步骤:
- 预处理:将头文件展开,去除注释,宏替换,条件编译,生成.i文件。
- 编译:词法分析,语法分析,语义分析,符号替换等,将代码翻译为汇编指令,生成.s文件。
- 汇编:将汇编指令翻译为二进制指令,生成.o文件。
- 链接:将生成的.o文件进行链接,形成可执行程序。
例如:我们想源文件test1.c,test2.c和main.c形成可执行文件,我们需要先得到各文件的目标文件test1.o,test2.o和main.o,然后将这些文件链接起来就可以得到可执行程序。
如果我们在另一个项目当中也需要用的test1.c和test2.c和该项目的main2.c形成可执行程序的话,步骤跟上面也是一样的:先生成.o文件,再链接得到可执行文件。
在现实生活的工作开发中,对于被频繁调用的源文件,我们一般会将他们的.o目标文件进行打包,在这里也就是将test1.o,test2.o进行打包,之后需要用这四个目标文件时就直接链接这个打包好的目标文件,而这个包就被称之为一个库。
其实,所有的库本质都是一堆.o目标文件的集合,库的文件当中不包含主函数只是包含大量的方法以供调用,实际上可以理解动静态库是可执行程序的半成品。
认识动静态库
我们先模拟一个场景,创建一个test.c文件,生成可执行程序,代码如下:
1 #include <iostream>2 using namespace std; 3 4 int main()5 {6 cout<<"hello world"<<endl;7 return 0;8 }
代码很简单,执行结果为:
我们可以调用cout函数输出文字,原因是g++编译器在编译生成可执行文件的时候,c++标准库也被链接进来了。我们可以通过ldd 文件名的方式来查看一个可执行程序依赖的库文件。
这里的libstdc++.so.6就是可执行程序依赖的c++标准库文件。
我们通过file 文件名的方式可以查看libstdc++.so.6的文件类型:
我们会发现libstdc++.so.6是一个共享库文件,其实准确的来说,这是一个动态库。
- Linux当中:.so为后缀的是动态库,.a为后缀的是静态库
- window当中,.dll为后缀的是动态库,.lib为后缀的是静态库
一个库的真正名字需要去掉前面的前缀,和后面的后缀及版本号才能得到。例如:libstdc++.so.6需要去掉前缀lib,后缀及版本号.so.6才能得到库的名字stdc++。
在gcc/g++中都是默认动态链接的,若想使用静态链接,在后面加上-static选项:
我们会发现静态链接的文件大小比动态链接的文件要大很多,原因我们一会儿再说。
静态链接生成的可执行文件并不依赖其他库文件,我们通过ldd命令可以看到它显示该文件不是一个动态链接的文件:
我们通过file命令查看两个文件类型也可以看到他们分别是动态链接和静态链接的:
动静态库的不同
静态库:
静态库是在链接的时候将库的代码复制到可执行程序中,生成的可执行程序在运行时就不再需要静态库,所以静态链接的可执行文件大小一般会大很多。
好处:
静态链接生成的可执行程序,不再需要库就可以独自运行
坏处:
会占用大量空间,特别是很多个静态链接的程序都需要的说同一个库,就会在内存中存在大 量的重复代码,例如:我有好几个程序都是需要cout打印个字符串,而静态链接为了打印这几 个小小的字符串要把c++标准库加载到内存好多好多份,这就导致了内存的极大浪费。
动态库:
动态库是程序在运行的时候才去链接对应的动态库代码,多个程序共享库的代码。一个与动态库链接的可执行文件仅仅包括它用到的函数入口地址的一个表,而不是外部函数所在目标文件的整个机器码。
在可执行文件运行前,外部函数的机器码由操作系统从磁盘上的动态库复制到内存中,这个过程被称为动态链接。动态库在多个程序之间共享,节省了磁盘空间,操作系统采用虚拟内存机制允许物理内存中的一份动态库被用到该库的所有进程共享,同时也节省了内存空间。
好处:
节省磁盘空间,且多个用到相同动态库的程序同时运行,内存中只有一份库代码不会出现重 复代码,因为库文件会通过进程地址空间进行共享。
坏处:
必须依赖动态库,否则无法运行。
静态库的打包与使用
为了演示,我们先写一些例子:创建4个文件,其中有add.c,add.h,sub.c,sub.h,以这四个文件做例子,四个文件的代码如下:
//add.h
#pragma once
#include<stdio.h>
extern int add(int x ,int y); //add.c
#include"add.h"
int add(int x ,int y)
{ printf("%d + %d = ?\n", x, y); return x+y;
}//sub.h
#pragma once
#include<stdio.h>
extern int sub(int x ,int y); //sub.c
#include"sub.h"
int sub(int x ,int y)
{ printf("%d - %d = ?\n", x, y); return x-y;
}
代码很简单,就是做最简单的加减法。
打包
下面我们将上面的四个文件打包生成一个静态库:
1.让所有源文件生成目标文件:
2.用ar命令将所ar有目标文件打包成静态库
ar是gnu的归档工具,常用于将目标文件打包成静态库,打包时我们一般会带上-r和-c选项。
我们可以这样记忆:
-r(replace),-c(create),若库文件中有当前目标文件就replace(更新),将旧的目标文件换成新的目标文件;若库文件中没有,就create,建立静态库文件。
我们还可以用ar命令带上-t和-v选项查看静态库中的文件。
-t:列出静态库中的文件
-v(verbose):显示详细的信息
3.将头文件和静态库组织起来
当我们想要把自己的库给别人用的时候,实际上需要给人家两个文件夹,一个文件夹下面放的是所有头文件的集合,另一个文件夹下放的是所有的库文件。
因此,我们这里将add.h和sub.h放在同一个目录myinclude下,将生成库文件放在另一个目录mylib下,将这两个目录都放在目录mathlib下,这样就可以把mathlib给别人使用了。
我们还可以将mathlib库通过tar指令将其打包压缩,之后就可以将打包文件放到yum源上,等别人想要使用的时候通过yum进行下载,或者可以将这个软件放在某个网站上供别人下载。
使用
现在我们站在用我们库的人的视角,看看怎样使用库。
我们先来在库所在目录下创建一个源文件main.c,用这个程序使用我们刚刚打包好的库。
#include <stdio.h>
#include <add.h>int main()
{int a = 20;int b = 10;int c = add(a, b);printf{"%d\n",c};return 0;
}
目录下现在包括main.c和静态库:
使用方法1:使用选项
需要三个选项:
- -I(include):指定头文件搜索路径
- -L(lib):指定库文件搜索路径
- -l(link):指明需要链接库文件路径下的哪一个库
这样我们就生成了可执行程序。
补充:
- 因为gcc不知道头文件add.h在哪里,所以需要指定头文件的搜索路径
- 头文件add.h中只有函数的声明,没有定义,所以还要指定所要链接的库文件的路径
- 工作中,一个库文件的lib目录下可能会有很多的库文件,所以我们需要指明我们需要链接的是具体哪一个库,将库文件名去掉前缀lib,再去掉后缀.so或.a以及版本号,剩下的就是库的名字。为什么头文件不用指明具体用哪个头文件呢?是因为在源文件中,我们都会写include代码表示我们需要用哪些库
- 选项-I,-L,-l后可以加空格,也可以不加
使用方法2:把头文件和库文件拷贝到系统路径下
既然编译器找不到我们自己的头文件和库文件,那么我们直接将头文件和库文件拷贝到系统路径下就好了。
更改系统文件,需要使用sudo命令。
尽管我们的头文件和库文件都拷贝到了系统路径下,但当我们使用gcc编译main.c文件生成可执行程序时,还是需要指明需要链接库文件的名字。
补充:
1.为什么都在系统路径下,c标准库就不用指明名字,但是我们自己的库就必须指明?
因为gcc就是用来编译C程序的,所以gcc编译的时候默认就找的是C库,但此时我们要链接的是哪一个库编译器是不知道的,因此我们还是需要使用-l
选项,指明需要链接库文件路径下的哪一个库。
2.我们将头文件和库文件拷贝到系统路径下的做法,就是在安装库,所谓的安装,本质就是拷贝!但我们不建议把自己写的库拷贝到系统路径下,这样会对系统文件造成污染。
动态库的打包和使用
打包
动态库的打包与静态库的打包有一些区别,我们依然用四个文件进行演示:
1.让所有源文件生成目标文件
与静态库不同的是,这里用源文件生成目标文件时需要带上-fPIC选项。
-fPIC(position independent code)目的是:生成位置无关码。
位置无关码是怎么回事,我们后面再解释。
2.使用gcc的 -shared 的选项将所有目标文件打包为动态库
生成静态库时,使用的时ar命令,但是生成动态库时,我们将gcc加上-shared选项即可。
3.将头文件和动态库组织起来
依然是创建一个目录mylib,在下面再创建两个目录分别放入头文件和动态库文件。(偷偷说一句,这个图有错误,我错把.o文件放在了mylib目录下,实际应该是将动态库文件放在该目录下)
使用
和静态库测试时一样,我们也在动态库所在目录下创建一个main.c文件用于测试。
和静态库链接时一样,我们可以使用-I,-L,-l,三个选项生成可执行程序,也可以先将头文件和库文件拷贝到系统目录下,然后只使用-l指明要链接的库的名字来生成可执行程序,这里我们使用第一种方法链接生成可执行程序:
生成了可执行文件,但是我们执行的时候会出现问题,该问题是我们无法找到动态库进行链接:
我们在编译的时候不都告诉编译器动态库的路径及名字了吗?为什么还是找不到呢?
原因是:我们在gcc编译的时候是告诉我们的编译器我们使用的头文件和库文件在哪以及叫什么,但是生成可执行文件以后,可执行文件就和编译器没有关系了,此后可执行程序运行起来后,操作系统就找不到可执行程序所依赖的动态库了,通过ldd命令我们也可以查看到我们的动态库是找不到的:
那么这个问题怎样解决呢?有三种方法:
一、更改LD_LIBRARY_PATH环境变量
LD_LIBRARY_PATH是程序运行态查找库时要搜索的路径,我们将动态库所在路径添加到该环境变量中即可:
(需要注意的是:这里我们添加的路径必须是绝对路径,而不是相对路径,因为环境变量本质其实是字符串嘛,不会按照相对路径的形式给你找的)
下载就可以正常运行我们的可执行程序:
但是这个方法还有一点需要注意,就是我们每次使用shell登陆,环境变量都会重置,也就是说这种方法只能在这会儿使用,假如我们将shell关了后重新登陆,就发现又不能用了,这就是因为环境变量在每次登陆的时候都会刷新导致的。
二、拷贝.so文件到系统共享库文件路径下
既然系统找不到我们的库文件,那我们直接将库文件拷贝到系统共享的库路径下,这样系统就能找到了:
但是这样做会污染系统文件,所以我们一般不这样做,大家实验完最好就把我们的这个小破库从里面删了:
三、配置/etc/ld.so.conf.d/
/etc/ld.so.conf.d/路径下存放的都是以.conf为后缀的配置文件,这些配置文件中存放的都是路径,系统会自动在/etc/ld.so.conf.d/路径下找所有配置文件里面的路径,之后就会在每个路径下查找你所需要的库。
我们可以将库文件目录的路径存如一个以.conf为后缀的文件中,再将.conf文件拷贝到/etc/ld.so.conf.d/下,这样在可执行程序运行时,系统也是能够找到我们的库文件的,这样做也是会污染系统文件,我们在这里就先不演示了。
位置无关码的解释
在栈区和堆区之间是共享区,一般动态库的代码都是映射在这个区域中,库文件也是个文件,它也会占用磁盘空间。当程序运行时,需要执行库当中的代码,本质就是进程来执行库中的代码。换言之,进程一旦运行起来,也要把库加载到内存中,当然也可以局部加载,然后将库映射到共享区,这时代码区的代码就可以直接访问共享区中的库。这样做的好处:当有多个进程时,可以统一把要使用的库从进程映射到自己的共享区中,这样的话,相当与可执行程序不需要携带库代码,从而有效地节省资源。
如果是静态链接,形成进程后就没有共享区,此时代码区包括自己的代码也包括库代码,这样的话假如有10个程序都有cout函数,程序就把每份c++标准库都拷贝一份到代码区,这时在内存中就会有10份重复的库代码,很浪费内存。所以动态库最大的特点就是所有使用同一种库的进程都可以把库从内存映射到共享区,这样只需要拷贝一份库代码,很节省内存空间。
但是,有可能进程1共享区只是一部分区域映射到某个库,进程2共享区也只是一部分区域映射到某个库,但是不管最终物理内存到虚拟地址是怎样映射的,可执行程序加载到物理内存中的任何位置,最后一定要保证库中产生的各种代码与这个库加载到内存中的位置和映射到共享区的位置是没有关系的,这就是产生位置无关码。简单理解就是库随时都可能被加载,它会被加载到物理内存中的某个位置,在物理内存中可能被不同进程的虚拟地址空间映射到共享区的任何位置,所以必须保证调用目标函数的地址它本身不会随着程序的加载位置和映射区域的位置变化而变化,这就是产生位置无关码。
总而言之:动态库是不随着本身加载到共享区的任意位置而会影响到库中代码地址发生变化,而导致库中的代码不可执行,所以 gcc 一定要用 fPIC 选项来产生与位置无关码。