1. 动静态库的介绍
一般而言,库分为动态库和静态库。
静态库在程序编译时被链接到目标代码中。一旦链接完成,静态库的代码就成为目标程序的一部分。这意味着如果多个程序都使用了同一个静态库,那么每个程序都会包含一份该库的副本,从而导致程序体积较大。
动态库在程序运行时被加载。多个程序可以共享同一个动态库,只有当程序运行时才会将动态库加载到内存中。这大大减小了程序的体积,同时也方便了库的更新和维护。
2. 动静态库的原理
我们知道,一个源文件变为一个可执行文件将经历四个步骤:
比如我们现在有test1.c
,test2.c
,test3.c
,以及main1.c
这四个.c
文件,经过预处理,编译,汇编之后分别生成test1.o
,test2.o
,test3.o
,以及main1.o
这四个.o
文件。最后经过生成a.out
的可执行文件。
但是此时我们的main2.c
文件的生成同时也需要依赖test1.c
,test2.c
,test3.c
这三个文件,生成可执行程序的步骤都是一样的。此时我们就可以选择将test1.c
,test2.c
,test3.c
这三个文件生成的test1.o
,test2.o
,test3.o
进行打包,之后再使用时,只需要链接这个"包"即可,这个"包"其实就是我们常说的库。
所以动静态库的本质其实是一堆xxx.o
文件的集合。对于库的使用,只需要提供头文件让使用者了解具体功能的作用。在编译程序时,通过链接指定的库来实现对库中功能的调用。
3. 动静态库的使用
在Linux
下,我们可以通过ldd 文件名
来查看一个可执行程序所依赖的库文件。这其中的libc.so.6
就是该可执行程序所依赖的库文件,我们通过ls命令可以发现libc.so.6
实际上只是一个软链接。
实际上该软链接的源文件libc-2.17.so
和libc.so.6
在同一个目录下,为了进一步了解,我们可以通过file 文件名
命令来查看libc-2.17.so
的文件类型。
通过上图观察,我们知道gcc/g++
编译器默认都是动态链接的,如果想使用静态链接,需要在后面加一个-static
。如果你并没有安装对应的静态库的话,可以使用以下指令安装。
其中需要注意的是:动静态库真实文件名需要去掉前缀lib
,再去掉后缀.so
或者.a
及其后面的版本号,比如说libc-2.17.so
就是C语言的标准库,其名为:c-2.17
。
4 动静态库的打包
为了方便更加深入理解动静态库,接下来我们以下文件为例,讲解一下我们如何将我们的文件打包成动静态库。
其中add.h
的内容如下:
#pragma once
extern int add(int x, int y);
其中add.c
的内容如下:
#include "add.h"
int my_add(int x, int y)
{
return x + y;
}
其中sub.h
的内容如下:
#pragma once
extern int sub(int x, int y);
其中sub.c
的内容如下:
#include "sub.h"
int sub(int x, int y)
{
return x - y;
}
4.1 静态库的打包
然后我们需要将add.h
,add.c
,sub.h
,sub.c
这个文件打包成静态库。
- 首先第一步将源文件生成对应.o文件。
- 第二步使用ar指令打包成对应的静态库。
其中ar
指令用法为ar 选项 库名 打包文件名
,其中又两个关键选项:
- 将头文件和生成的静态库组织起来。
当把自己的库提供给他人使用时,通常需要给予两个文件夹:
最后,将这两个目录(include
和lib
)都放置在mathlib
目录下,此时就可以把mathlib
提供给别人使用了。
为了方便我们处理,我们可以写一个Makefile
。
libmath.a:add.o sub.o
ar -rc libmath.a $^
%.o:%.c #展开所以.c文件生成对应的.o文件
gcc -c $^
.PHONY:clean
clean:
rm -rf ./*.o mathlib
.PHONY:output #发表库
output:
mkdir -p mathlib/include
mkdir -p mathlib/lib
cp ./*.h mathlib/include
mv ./*.a mathlib/lib
4.2 静态库的使用
我们如果使用我们打包的静态库,在使用gcc
编译时需要带有以下三个选项:
由于在程序执行时,编译器并不知晓我们所声明的头文件以及链接库的具体位置,而且链接库中可能存在不同的库文件。因此,我们需要在命令行中指定头文件的搜索路径,库文件的搜索路径,以及具体使用哪个库。
比如我们需要执行main.c
,其中main.c
中使用静态库中的add
函数。
#include<stdio.h>
#include"add.h"
int main()
{
int a=1;
int b=2;
int ret=add(a,b);
printf("%d\n",ret);
return 0;
}
其中需要注意的是,-I
,-L
,-l
这三个选项后面可以加空格,也可以不加空格。
那么我们就有个疑问,那就是我们使用gcc
编译文件时为什么没有带-I
,-L
,-l
这三个选项呢?
其实很简单,因为我们之前使用的库都默认在系统的路径下: 编译器能准确识别这些存在于配置文件中的路径。其实如果为了方便我们也可以将头文件和库文件拷贝到系统路径/usr/include
,/lib.64
下:
这时再使用gcc
编译时就只需要带-l
选项,指明链接库文件下具体哪个库。
但是实际上,我们并不推荐将自己写的头文件和库文件拷贝到系统路径下,因为这样做可能会对系统文件造成污染。
4.3 动态库的打包
动态库的打包相对于静态库较为复杂,但大致相同,我们还是利用add.h
,add.c
,sub.h
,sub.c
这四个文件进行打包演示:
- 首先第一步将源文件生成对应.o文件。
但是与静态库不同的是,需要带-fPIC
选项,因为动态库运行时才会被加载。
- 第二步:使用-shared选项将所有目标文件打包为动态库。
生成对应的动态库并不需要使用ar
指令,还是使用gcc
编译,只不过需要带-shared
选项。
- 将头文件和生成的动态态库组织起来。
与静态库类似,当把自己的库提供给他人使用时,通常需要给予两个文件夹:
最后,将这两个目录(include
和lib
)都放置在mathlib
目录下,此时就可以把mathlib
提供给别人使用了。
同样为了方便管理,我们也可以定义一个Makefile
文件。
libmath.so:add.o sub.o
gcc -shared -o $@ $^
%.o:%.c
gcc -fPIC -c $<
.PHONY:clean
clean:
rm -rf ./*.o mathlib
.PHONY:output
output:
mkdir -p mathlib/include
mkdir -p mathlib/lib
cp ./*.h mathlib/include
mv ./*.so mathlib/lib
4.4 动态库的使用
我们如果使用我们打包的动态库,使用gcc
编译时同样需要带有以下三个选项:
因为在程序执行时,编译器同样并不知晓我们所声明的头文件以及链接库的具体位置,而且链接库中可能存在不同的库文件。因此,我们需要在命令行中指定头文件的搜索路径,库文件的搜索路径,以及具体使用哪个库。
比如我们需要执行main.c
,其中main.c
中使用动态库中的add
函数。
#include<stdio.h>
#include"add.h"
int main()
{
int a=1;
int b=2;
int ret=add(a,b);
printf("%d\n",ret);
return 0;
}
但是与静态库不同的是,我们并不能直接执行a.out
这个可执行文件。
为什么使用了-I
,-L
,-l
这三个选项,还是没有找到对应的动态库呢?
为了解决这个问题,我们有三种方法:
- 第一种就是将库文件拷贝到系统共享的库路径下。
但是这种方法可能会对系统文件造成污染,所以我们一般不采取该方法。
- 第二种就是更改环境变量LD_LIBRARY_PATH。
LD_LIBRARY_PATH
是程序运行动态查找库时所要搜索的路径,我们只需将动态库所在的目录路径添加到LD_LIBRARY_PATH
环境变量当中,程序运行起来时就能找到对应的路径下的动态库。
但是我们知道环境变量在重启时会自动恢复,所以这种方法只在当前状态下有效,具有临时性。
- 配置.conf/文件
在系统中,/etc/ld.so.conf.d/
是用于搜索动态库的路径。此路径下存放的全是后缀为.conf
的配置文件,这些配置文件中所存放的内容都是动态库的路径。
因此,若将自己库文件的路径也放置在该路径下,那么当可执行程序运行时,系统就能够找到我们的库文件。并且这种行为是永久的,并不会随重启而改变。
首先我们将对应的库文件所在地址写入一个.conf
文件中,然后将其导入/etc/ld.so.conf.d/
路径,最后使用指令ldconfig
更新一下配置文件,最后我们就能执行我们的可执行文件了。