一、函数库简述
简单来说,函数库就是一些事先写好的模块化的函数的集合,可以供给其他程序员使用。最开始没有函数库,每个程序员写程序都要从零开始写,时间长了慢慢地就积累下来了一些优质的函数库。后来有组织把各种函数库收拢在一起,经过校准和整理,形成一份标准化的函数库,就是现在的标准的函数库,如glibc
1.1 函数库的提供形式:动态链接库与静态链接库
早期的函数共享都是以源代码的形式进行的,慢慢地,源码共享的方向就形成了我们现在的开源社区,无法以商业化形式来发布函数库。商业公司需要将自己的函数库让他人付费使用,但是又不能给客户源代码,这时候就出现了静态链接库和动态链接库。
1.1.1 静态链接库
-
商业公司将函数库源代码经过只编译不连接形成
.o
的目标文件,然后用ar
工具将.o
文件归档成.a
的归档文件,这个.a
的归档文件就叫做静态链接库文件。通过发布.a
库文件和.h
头文件来提供静态库给客户使用; -
客户拿到
.a
和.h
文件后,通过.h
头文件得知库中的库函数的原型,然后在自己的.c
文件中直接调用这些库文件,在链接的时候链接器会去.a
文件中拿出被调用的那个函数的编译后的.o
二进制代码段链接进去形成最终的可执行程序。
1.1.2 动态链接库
-
动态库比静态链接库出现得晚一些,效率更高一些。现在一般都是使用动态库。静态库在用户链接自己的可执行程序时就已经把调用的库中的函数的代码段链接进最终可执行程序中了,这样好处是随处可以执行,坏处是太占地方了。尤其是有多个应用程序都使用了同一个库函数时,最后生成的可执行程序中都各自有一份这个库函数的代码段。当这些应用程序同时在内存中运行时,实际上在内存中有多个这个库函数的代码段,这完全重复了。
-
而动态链接库本身不将库函数的代码段链接入可执行程序,只是做个标记。然后当应用程序在内存中执行时,运行时环境发现它调用了一个动态库中的库函数时,会去加载这个动态库到内存中,然后以后不管有多少个应用程序去调用这个库中的函数都会跳转到第一次加载的地方去执行(不会重复加载)。
1.2 函数库中库函数的使用
gcc
中编译链接程序默认使用动态库,要静态链接需要显式用-static
来强制静态链接。库函数的使用需要注意:
- 包含相应的头文件
- 调用库函数时注意函数原型
- 有些库函数链接时需要额外用
-lxxx
来指定链接- 如果不是系统库,要注意用
-L
指定库的地址
如我们使用数学函数库中的sqrt
函数:
- 真正的数学运算的函数定义在:
/usr/include/x86_64-linux-gnu/bits/mathcalls.h
- 使用数学库函数的时候,只需要包含
math.h
即可
#include <stdio.h>
#include <math.h>
int main(void)
{
double x = 16.0;
double y = sqrt(x);
printf("y = %lf\n", y);
return 0;
}
学过C语言的同学都知道答案是4.000000
,但是直接编译的结果是什么呢?看下图:
我们会发现在链接的时候报错了!报的是对'sqrt'
未定义的引用,即sqrt函数有声明(在math.h
中)有引用(在math.c
),但是没有定义,链接器找不到函数体。为什么会这样呢?因为库函数有很多,链接器去库函数目录搜索的时间比较久。为了提升速度链接器只是默认地寻找几个最常用的库,如果是一些不常用的库中的函数被调用,需要程序员在链接时明确给出要扩展查找的库的名字。
链接时可以用-lxxx来指示链接器去到libxxx.a(静)或libxxx.so(动)中去查找函数
我们可以看到加了-lm
后就成功链接了,同时,我们也可以看到静态链接比动态链接生成的可执行文件的大小根本不在同一个数量级!!!
二、制作并使用静态链接库
2.1 制作
创建对应的.c源文件
和.h头文件
以及Makefile
:
touch mylib1.c mylib1.h Makefile
编辑好头文件和源文件的对应内容,如图所示:
接下来先只编译不连接,生成.o
文件;然后使用ar
工具进行打包成.a
归档文件,打包时注意库名不能随便乱起,一般是lib+库名称
,后缀名.a
表示是静态链接库文件。对应的Makefile
段如下所示:
lib:
gcc -c mylib1.c -o mylib1.o
ar -rc libmylib1.a mylib1.o
执行对应的脚本即可完成静态库的制作
2.2 发布
所谓发布,就是将.a
文件和.h
文件放在一个独立的文件夹里面,以便后续使用。如图所示
3.3 使用
我们在发布的文件夹里建立自己的.c
文件,就可以直接调用库函数了!
touch main.c Makefile
我们在主函数中添加如下内容:
#include "mylib1.h"
int main(void)
{
myprint();
return 0;
}
在1.2
使用数学函数的时候我们知道链接器只会去器寻找几个最常用的库,我们发布的库肯定不会被寻找的,所以需要加-lxxx
去指定
all:
gcc main.c -o main.elf -lmylib1
我们会发现,这样还是不能正确链接,会有如下报错信息:/usr/bin/ld: 找不到 -lmylib1
,通过报错信息我们知道,链接器默认只在系统指定的目录下寻找链接库。所以需要用-L指定库的地址,我们将Makefile修改如下:
all:
gcc main.c -o main.elf -lmylib1 -L.
其中.
就表示在当前目录下,我们可以看到程序进行了成功地编译链接
三、制作并使用动态链接库
3.1 制作与发布
动态链接库的制作和静态库思路类似,只是后缀名是.so
(windows
下是dll
),通过gcc
来打包而不是ar
,对应的Makefile
如下所示:
lib:
gcc -c mylib2.c -o mylib2.o -fPIC
gcc mylib2.o -o libmylib2.so -shared
其中:
-fPIC
是位置无关码,作用于编译阶段,告诉编译器产生与位置无关的代码(Position-Independent Code)
。即产生的代码中,没有绝对地址,全部使用相对地址,被加载器加载到内存的任意位置,都可以正确执行。这正是共享库所要求的,共享库被加载时,在内存的位置不是固定的。-shared
是按照共享库的方式来链接
发布的时候发布libxxx.so
和xxx.h
即可,这里不再赘述
3.2 使用
有了静态库的经验我们知道,编译链接时的指令应该是:
all:
gcc main.c -o main.elf -lmylib2 -L.
我们可以看到程序被成功编译链接了
但是运行出错,报错信息:error while loading shared libraries: libmylib2.so: cannot open shared object file: No such file or directory
这是因为动态链接库运行时需要被加载(运行时环境在执行程序的时候发现动态链接了libxxx. so
,于是会去固定目录/usr/lib
下加载libxxx. so,如果加载失败则会打印以上错误信息。)
解决方法
- 将libaston.so放到固定目录/usr/lib下
cp libmylib2.so /usr/lib
一般而言固定目录/usr/lib
用来存放系统自带的动态链接库,不建议用户自行修改里面的内容
- 使用环境变量LD_LIBRARY_PATH
操作系统在加载固定目录/usr/lib
之前,会先去LD_LIBRARY_PATH
这个环境变量所指定的目录下去寻找,如果找到则不去/usr/lib
目录下,如果没找到再去/usr/lib
下。所以可将libaston.so
所在的目录导出到环境变量LD_LIBRARY_PATH
中。
export LD_LIBRARY_PATH=D_LIBRARY_PATH:\
/mnt/hgfs/VMShare/C/6.PreprocessFunction/funclib/dynamiclib/publib
我们可以发现,导入环境变量之后程序就可以直接运行了
3.3 ldd命令
ldd
命令可以在一个使用了共享库的程序执行之前解析出这个程序使用了哪些共享库,并且查看这些共享库是否能被找到,如上述示例加入了共享库后,使用了的所有的.so
都能被解析到地址
如果我们将环境变量设置为空:
export LD_LIBRARY_PATH=
我们可以看到main.elf
链接的libmylib2.so
处于not found
状态,故无法正确执行