前言
对于C/C++的学习者,我们经常听到C/C++的标准库,我们也经常使用它们,但是我们在使用的时候经常只包含一下头文件,然后就使用了,我们从来没有认真的研究过C/C++的标准库,而且C/C++的头文件中只有声明并没有声明的内容的具体实现,为什么我们只包含头文件就能使用库呢?本篇文章我们就来一起探讨一下。
静态库与动态库
- 一、什么是库
- 二、怎么制作一个库
- 1、静态库
- 2、静态库的使用
- 3、 动态库
- 4、动态库的使用
- 三、动静态库的加载
- 1、静态库的加载
- 2、动态库的加载
- 四、一些其他结论
一、什么是库
简单来说:库是一些可重定向的二进制文件,这些文件在链接时可以与其他的可重定向的二进制文件一起链接形成可执行程序。
一般来说库被分为静态库和动态库,他们是有不同的后缀来进行区分的。
系统平台 | 静态库 | 动态库 |
---|---|---|
Windows | .lib | .dll |
Linux | .a | .so |
另外对于C/C++来说其库的名称也是有规范要求的,例如在Linux
下:一般要求是lib + 库的真实名称 +(版本号)+ .so /.a + (版本号)
,版本号是可以省略不写的。
例如这两个标准库 :
libstdc++.so.6 真实名称是 c++
libc-2.17.so 真实名称是 c
头文件与库的关系
- 头文件提供方法说明,库提供方法的实现,头和库是有对应关系的,是要组合在一起使用的
- 头文件是在预处理阶段就引入的,程序在链接时链接的本质其实就是链接库!
有了上面的一点基础知识以后我们就能够去见一见库了,Linux
系统在安装时已经为我们预装了C&C++的头文件和库文件。
对于C/C++头文件在Linux
里面一般在/usr/include
目录下面存放我们的头文件
对于C/C++的库文件,一般在/usr/lib64
和 /lib64
里面,/lib64里面给的是root和内核所需so或者a之类的库文件,而/usr/lib64是普通用户能够使用的。
到这里我们也能够理解一些现象了:
-
我们在使用像vs2019这样的编译器时要下载并安装开发环境,这其中是在下载什么?
答案是:安装编译器软件,安装要开发的语言配套的库和头文件。 -
我们在使用编译器,都会有语法的自动提醒功能,但是都需要先包含头文件,这时为什么呢?
答案是:语法提醒本质是编译器或者编辑器,它会自动的将用户输入的内容,不断的在被包含的头文件中进行搜索,自动提醒功能是依赖头文件而来的! -
我们在写代码的时候,我们的环境怎么知道我们的代码中有哪些地方有语法报错,哪些地方定义变量有问题?
答案是:不要小看编译器,编译器有命令行的模式,还有其他自动化的模式,编辑器或集成开发环境可以在后台不断的帮我们调用编译器检查语法而不生成可执行文件,从而达到语法检查的效果。
二、怎么制作一个库
库的使用能够提高我们的开发效率,接下来我们来制作一个库!
1、静态库
静态库:程序在编译链接的时候把库的代码链接到可执行文件中。程序运行的时候将不再需要静态库,当然这也会导致我们编译出来的可执行程序会变大。
看下面一段代码来演示库:
头文件:
源文件:
主程序:
文件目录结构:
可以看到我们将库的头文件与库的实现文件放在了mylib
文件夹里面了,将main.c
放在了otherPerson
里面,此时mian.c
与库的头文件以及实现文件不在一起,此时编译会报错。
提示我们找不到头文件,就算我们将头文件移过去也会有链接错误,如果我们不想将源码给otherPerson
,但是我们又想要main.c能完成编译形成可执行程序,我们就要对我们的库文件的实现进行编译但不生成最终的可执行程序。
然后我们将我们的头文件和可重定向的二进制文件进行拷贝到otherPerson
里面
再进行编译链接,我们就形成了可执行程序test
运行test
,程序执行成功
上面的整个过程就是我们制作静态库的基本流程,当然这样的制作其实还是有缺陷的,当我们的项目文件过于庞大时,我们要给一个.c
文件十几个这样的.o
文件,而且文件过于分散了,不利于管理,于是我们就需要将多个这样的.o
文件打成一个包,我们将这个包直接给别人,别人就能直接使用了。
打包的命令是:ar -rc
命令
ar
命令用于建立或修改备存文件,或是从备存文件中抽取文件。可集合许多文件,成为单一的备存文件,在备存文件中,所有成员文件皆保有原来的属性与权限。
r
:如果打包好的xxx.a
库中没有xxx.o
那么就会把模块xxx.o
添加到库的末尾,如果有的话就会替换之(位置还是原来的位置)。
c
:建立备存文件。
于是我们就尝试将原来的.o
文件进行打包,不过要注意的是打包好的库要遵循库的命名规范。
此时我们的当前目录里面就有了静态库了,当我们删除静态库以后,我们用静态库生成的可执行程序还能够正常运行,这就是静态库的特点!!!
2、静态库的使用
指明路径
在我们实际使用库时,我们一般将头文件放在一个目录里面,将库放到另外一个文件里面,这样便于我们进行分类管理。我们也按照这种标准化的做法,来整理一下我们的目录结构。
我们libmymath.a
静态库不是C的标准库,所以gcc
不会在进行编译时去链接我们自己写的静态库,所以我们还要给gcc
添加一些参数用来指明我们要链接的静态库。
(其中 -I
- L
-l
,其后面传递的内容可以加空格进行分割,也可以不加空格)
-I
: 指明我们要包含的头文件路径
-L
:指明我们包含的库的路径
-l
:指明我们要包含的库文件名(这里的库文件名是指真实名称)
./test
运行我们的程序,发现程序可以正常启动。
转移到系统的默认搜索路径中
对于C/C++的头文件Linux
系统默认搜索路径是 :/usr/include
。
对于C/C++的库文件Linux
系统默认的搜索路径是:/usr/lib64
和 /lib64
。
我们将文件移动到对应的默认搜索路径中:
此时我们再进行编译我们的mian.c
这时gcc
编译器报链接错误,提示我们找不到库,这时因为我们使用的是第三方库,编译时无论如何都要指明文件名!
指明路径以后,我们就能够正常编译了!
总结:第三方库的使用
- 需要指定的头文件,和库文件
- 如果没有默认安装到系统
gcc
、g++
默认的搜索路径下,用户必须指明对应的选项,告知编译器: a.头文件在哪里 b.库文件在哪里 c.库文件具体是谁 - 将我们下载下来的库和头文件,拷贝到系统默认路径下,在
Linux
下就是安装库! 那么卸载呢?对任何软件而言,安装和卸载的本质就是拷贝到系统特定的路径下! - 如果我们安装的库是第三方的库,我们要正常使用,即便是已经全部安装到了系统中,
gcc
g++
必须用-l
指明具体库的名称!
3、 动态库
动态库:程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码。如果我们删除动态库,则使用动态库生成的可执行程序都将无法运行!
我们制作动态库时不再需要ar
命令,我们需要一下两个步骤:
- 在
gcc
形成二进制文件时加上-fPIC
参数,这样产生的可重定向二进制文件就会形成与位置无关码。 - 然后用
gcc
加上-shared
参数,把所有有与位置无关码的可重定向二进制文件进行打包形成一个动态库。
先看一下我们的目录结构:
形成与位置无关码:
打成动态库:
当我们有了动态库以后,我们是可以删除可重定向的二进制文件的,但是动态库不能够删除,动态库删除的话,依赖此动态库的程序也将不能够运行!
下面我们尝试用动态库去链接形成可执行程序:
注意:我们自己写的库是属于第三方库,我们要编译时要指明:头文件路径,库文件路径,库文件名(真实名称)。
4、动态库的使用
可以看到我们已经使用动态库编译成功了,下面我们来运行一下我们的程序。
发生了错误,系统提示我们程序运行时,没有办法找到动态库,这是为什么呢?
这就和动态库的特性有关了,由于采用动态库的程序在运行的时候才去链接动态库的代码,多个程序共享使用库的代码,所以运行的程序必须要知道去哪里链接我们的库,即对于动态库在编译期间我们要告诉编译器去哪里链接库进行编译,在运行期间要告诉操作系统去哪里链接库进行运行。
静态库不需要链接是因为:静态库在编译链接期间将用户使用的二进制代码直接拷贝到目标可执行程序中,编译后的程序是一个完整的程序,不需要再运行时再使用静态库了。
解决操作系统找不到动态库的方法有三种:
环境变量
在我们Linux
下有一个环境变量:LD_LIBRARY_PATH
,操作系统会去这个环境变量下的路径去搜索动态库,我们可以将我们的第三方库加入到这个环境变量中,然后我们再运行我们的可执行程序就能成功了。
执行我们的程序:
程序执行成功!
但是我们都知道,环境变量只在一次登录内有效,如果我们采用环境变量的方法我们下次登录时依旧无法运行,所以这种方法具有临时性。
软连接
我们知道Linux
中C/C++的默认库路径是/usr/lib64
或/lib64
,这也是系统搜索库的默认路径,我们可以将我们的第三方库在这个目录下面建立一个软连接(不推荐直接将第三方库拷贝到默认库路径/usr/lib64
或/lib64
下面),这样我们也能够正常使用了。
执行我们的程序,正常运行,软链接一个比较好的寻找库目录方法。
修改配置文件
在我们的Linux
系统中有一个配置文件目录/etc/ld.so.conf.d
,在这个目录里面我们可以创建一个文件,文件里面写上动态库的路径,这样我们系统在搜索动态库时也会搜索到该路径。
创建文件并填上我们的路径:
这样我们是配置文件就修改完毕了,但是我们还要让配置文件立即生效,我们可以使用ldconfig
命令。
一切准备好以后我们就可以运行我们的程序了!
三、动静态库的加载
1、静态库的加载
在形成可执行程序的链接期间,静态库中的代码会被直接拷贝一份进入可执行程序内。所以在程序运行期间静态库可以理解为不会被加载,或者说静态库和程序一起被加载。
但是由于是静态库,当多个进程包含相同的静态库时这会导致内存中存在大量的重复代码,导致内存资源的浪费。
2、动态库的加载
采用动态库的程序在使用库中的方法时,会在使用的地方留下一个标记,在程序运行以后进行动态链接时,会将这个标记替换为动态库中的地址。
当一个使用了动态库的进程A运行起来以后在需要动态库a时,操作系统会先在内存中搜寻a,是否存在,如果存在,就直接将a通过页表进行映射进进程A的进程地址空间中的共享区中,如果不存在就会将磁盘中的动态库a加载进入内存,然后再通过页表进行映射。
我们知道被编译好的程序内部是有地址的!动态库内部的地址并不是绝对地址,而是偏移量!(相对地址)
因为不同的进程,运行程度不同,需要使用的第三库是不同的注定了,每一个进程的共享空间中空闲位置也是不确定的!如果采用了绝对编址,在一个进程使用了多个库时就有可能照成地址冲突!
当一个动态库,真正的被映射进地址空间的时候,它的起始地址才能真正确定! 此时动态库中的方法的地址就等于库的地址加上自己在库中的偏移量。通过这种设计方式,动态库在进程的地址空间中,可以随便加载,我们都能够找到库中的方法,也不会与其他库产生冲突了! 这就是与位置无关码。
四、一些其他结论
- 动态库和静态库同时存在,系统默认采用动态链接
- 一般来说可执行程序在生成时,会对多个库进行链接,我们可以使用
ldd
命令查看我们的程序链接了那些库,可执行程序在连接时也可以选择部分采用动态库部分采用静态库。 - 如果我们对
gcc
加上了-static
参数,gcc
就会默认帮我们全部采用静态链接的方式链接库。