库是写好的现有的,成熟的,可以复用的代码。现实中每个程序都要依赖很多基础的底层库,不可能每个人的代码都从零开始,因此库的存在意义非同寻常。
本质上来说库是一种可执行代码的二进制形式,可以被操作系统载入内存执行。库有两种:静态库(.a、.lib)和动态库(.so、.dll)。
一个程序编译成可执行程序的步骤:
在链接阶段,会将汇编生成的目标文件.o与引用到的库一起链接打包到可执行文件中,因此对应链接方式称为静态链接。
静态库与汇编生成的目标文件一起链接为可执行文件,那么静态库必定与.o文件格式相似。
一个静态库可以简单视作一组目标文件(.o/.obj文件)的集合,即很多目标文件经过压缩打包后形成的一个文件。
静态库特点:
- 静态库对函数库的链接是放在编译时期完成的。
- 程序在运行时与函数库再无瓜葛,移植方便。
- 浪费空间和资源,因为所有相关的目标文件与牵涉到的函数库被链接合成一个可执行文件。
Linux下使用ar
工具,将目标文件压缩到一起,并且对其进行编号和索引,以便于查找和检索。
一般创建静态库步骤如图所示
Linux静态库命名规范,必须是"lib[your_library_name].a":lib为前缀,中间是静态库名,扩展名为.a。
静态库容易使用和理解,也达到了代码复用的目的,那么为什么还需要动态库呢?
静态库存在一些不足:
- 空间浪费。静态库在内存中存在多份拷贝导致空间浪费。 例如,静态库占用1M内存,有2000个这样的程序,将占用近2GB的空间。
- 静态库对程序的更新、部署、和发布页带来麻烦。 如果静态库liba.lib更新了,所以使用它的应用程序都需要重新编译、发布给用户(对于玩家来说,可能是一个很小的改动,却导致整个程序重新下载,全量更新)。
动态库在程序编译时并不会被连接到目标代码中,而是在程序运行是才被载入。
不同的应用程序如果调用相同的库,那么在内存里只需要有一份该共享库的实例,规避了空间浪费问题。动态库在程序运行是才被载入,也解决了静态库对程序的更新、部署和发布页会带来麻烦。用户只需要更新动态库即可,增量更新。
动态库在内存中只存在一份拷贝,避免了静态库浪费空间的问题。
动态库特点:
动态库把对一些库函数的链接载入推迟到程序运行的时期。
可以实现进程之间的资源共享。(因此动态库也称为共享库)
将一些程序升级变得简单。
甚至可以真正做到链接载入完全由程序员在程序代码中控制(显示调用)。
Window与Linux执行文件格式不同,在创建动态库的时候有一些差异。
在Windows系统下的执行文件格式是PE格式,动态库需要一个DllMain函数做出初始化的入口,通常在导出函数的声明时需要有_declspec(dllexport)关键字。
Linux下gcc编译的执行文件默认是ELF格式,不需要初始化入口,亦不需要函数做特别的声明,编写比较方便。
与创建静态库不同的是,不需要打包工具(ar、lib.exe),直接使用编译器即可创建动态库。
动态链接库的名字形式为 libxxx.so,前缀是lib,后缀名为".so"。
我们通常把一些公用函数制成函数库,供其他程序使用。
函数库分为静态库和动态库两种。
静态库:在程序编译时被连接到目标代码中,程序运行时将不再需要该静态库。
动态库:在程序编译时不会被连接到目标代码中,而是在程序运行时才被载入,因此在程序运行时还需要动态库存在。
下面将通过举例说明在Linux中如何创建静态库和动态库,及其使用。
在创建函数库之前,需要先准备示例源程序,并将函数库源程序编译为.o文件。
先创建一个作业目录,保存练习的文件。
mkdir test1
cd test1
注:
Linux mkdir(make directory)命令用于创建目录,
语法:mkdir [-p] dirName
-p确保目录名称存在,不存在就建一个
示例:mkdir -p fun/test
若fun目录原本不存在,则需要建立一个,如果该示例中不加-p参数,且原本fun目录不存在,则可能会产生错误。
Linux cd(change directory)命令用于切换当前工作目录
语法:cd [dirName]
dirName表示要切换的目标目录,可以为绝对路径或相对路径,若目录名称省略,则变换至使用者的home目录(即login时所在目录)
另外,~
也可表示为home目录的意思,.
表示目前所在目录,..
表示目前目录位置的上一层目录。
示例:
cd /usr/bin 跳到/usr/bin/
cd ~ 跳到自己的home目录
cd …/… 跳到目前目录的上上两层
用vim、nano或gedit等文本编辑器生成所需要的三个文件:hello.h,hello.c,main.c
这里我以vim编辑器为例,分别输入vim hello.h
, vim hello.c
, vim main.c
程序代码如下。
/*hello.h*/ #ifndef HELLO_H #define HELLO_H void hello(const char *name); #endif //HELLO_H
/*hello.c*/ #include <stdio.h> void hello(const char *name) { printf("Hello %s!\n", name); }
/*main.c*/ #include "hello.h" int main() { hello("everyone"); return 0; }
hello.h为该函数库的头文件。main.c为测试库文件的主程序,
在主程序中调用了公用函数 hello。
无论静态库还是动态库,都是由.o文件创建的。因此,必须将源程序hello.c通过gcc先编译成.o文件,在系统提示符下键入以下命令得到hello.o
文件。
gcc -c hello.c
(运行ls命令查看是否生成了hello.o文件)
ls
得到结果:hello.c hello.h hello.o main.c
实际运行情况如下图所示
我们可以看到hello.o文件的生成
静态库文件名的命名规范是以 lib 为前缀,紧接着跟静态库名,扩展名为.a。
例如:我们将创建的静态库名为 myhello
,则静态库文件名就是 libmyhello.a
。在创建和使用静态库时,需要注意这点。
创建静态库用 ar
命令。
下面我们来创建静态库文件libmyhello.a。
ar -crv libmyhello.a hello.o
(同样,运行ls命令查看结果)
ls
得到结果:hello.c hello.h hello.o libmyhello.a main.c
实际运行情况如下图所示
ls命令结果中有libmyhello.a
如何使用静态库的内部函数呢?
需要在使用到这些公用函数的源程序中包含这些公用函数的原型声明,然后再用gcc命令生成目标文件时指明静态库名,gcc将会从静态库中将公用函数连接到目标文件中。
注意,gcc会在静态库名前加上前缀lib,然后追加扩展名.a得到的静态库文件名来查找静态库文件。
在main.c
中,我们包含了静态库的头文件 hello.h
,然后在主程序 main 中直接调用公用函数 hello。
下面先生成目标程序 hello,然后运行 hello 程序。
方法一
gcc -o hello main.c -L. –lmyhello
自定义的库时,main.c 还可放在-L.和 –lmyhello 之间,但是不能放在它俩之后,否则会提示 myhello 没定义
但是是系统的库时,如g++ -o main(-L/usr/lib) -lpthread main.cpp
就不出错。
方法二
gcc main.c libmyhello.a -o hello
方法三
先生成 main.o
gcc -c main.c
再生成可执行文件
gcc -o hello main.o libmyhello.a
动态库连接时也可以这样做。
这里以方法一为例,实际运行效果如下,输入./hello
,返回Hello everyone!
删除静态库文件,来确定公用函数hello是否真的连接到目标文件hello中
rm libmyhello.a
./hello
返回结果:Hello everyone!
实际运行效果如下图所示
程序照常运行,静态库中的公用函数已经连接到目标文件中了。
如何在 Linux 中创建动态库。我们还是从.o 文件开始。
动态库文件名命名规范和静态库文件名命名规范类似,也是在动态库名增加前缀 lib,但其
文件扩展名为.so。
例如:我们将创建的动态库名为 myhello,则动态库文件名就是 libmyhello.so。
用 gcc来创建动态库。
键入以下命令得到动态库文件libmyhello.so
gcc -shared -fPIC -o libmyhello.so hello.o (-o 不可少)
ls
返回结果:hello.c hello.h hello.o libmyhello.so main.c
实际运行效果如下图所示
大一点的项目会编写makefile文件(CMake等等工程管理工具)来生成静态库。
在程序中使用动态库和使用静态库完全一样,也是在使用到这些公用函数的源程序中包含
这些公用函数的原型声明,然后在用 gcc 命令生成目标文件时指明动态库名进行编译。
我们运行 gcc 命令生成目标文件,再运行它观察结果。
gcc -o hello main.c -L. -lmyhello
或 gcc main.c libmyhello.so -o hello (没有 libmyhello.so 的话,会出错)
但是之后./hello
会报错,因为虽然连接时用的是当前目录的动态库,但是运行时,是到/usr/lib 中找库文件的,将文件libmyhello.so
复制到目录/usr/lib
中就可以了。
mv libmyhello.so /usr/lib
./hello
得到结果:Hello everyone!
虽然按照教程可以得到如下结果,但我在运行中仍报错,无法进行文件的复制,提示为Permission denied
,原因是权限不够。
网上找到的解决方法有
sudo chmod -R 777 usr
(-R是指级联应用到目录里的所有子目录和文件,777 是所有用户都拥有最高权限)
但仍出现问题。
因此我直接选用root身份,输入su root
,此时拷贝文件成功!
这是操作过程遇到的问题和解决操作
终于成功!
进一步说明动态库在程序运行时是需要的。
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/home/example/lib
/etc/ld.so.conf
文件,把库所在的路径加到文件末尾,并执行 ldconfig 刷新。这样,加入的目录下的所有库文件都可见。注:像下面这样指定路径去连接系统的静态库,会报错说要连接的库找不到:
g++ -o main main.cpp -L/usr/lib libpthread.a
必须g++ -o main main.cpp -L/usr/lib -lpthread
才正确 。
自定义的库考到/usr/lib 下时,
g++ -o main main.cpp -L/usr/lib libpthread.a libthread.a libclass.a
会出错,
但是g++ -o main main.cpp -L/usr/lib -lpthread -lthread -lclass
就正确了。
先删除除.c和.h外的所有文件,恢复成我们第一步的操作。
rm -f hello hello.o /usr/lib/libmyhello.so
ls
得到结果:hello.c hello.h main.c
实际运行结果如下:
本想退出root用户模式进行操作,但是权限仍会报错,于是仍然在root模式下进行。
再来创建静态库文件libmyhello.a
和动态库文件libmyhello.so
gcc -c hello.c
ar -cr libmyhello.a hello.o
gcc -shared -fPIC -o libmyhello.so hello.o
ls
得到结果hello.c hello.h hello.o libmyhello.a libmyhello.so main.c
可以发现静态库文件和动态库文件都已生成,并都在当前目录下。
我们运行gcc命令使用函数库myhello生成目标文件hello,并运行。
gcc -o hello main.c -L. -lmyhello
./hello
结果报错
动态库和静态库同时存在时,优先使用动态库,当然,如果直接gcc main.c libmyhello.a -o hello
的话,就是指定为静态库了。
从程序 hello 运行的结果中很容易知道,当静态库和动态库同名时,gcc 命令将优先使用动态库,默认去连/usr/lib 和/lib 等目录中的动态库,将文件 libmyhello.so 复制到目录/usr/lib中即可。
之后我又键入了ldd hello
命令,查看依赖关系
在实际linux开发与调试中, 要经常查看动态库依赖关系, ldd用得还是比较多的。
命令 | 解析 |
---|---|
-shared | 指定生成动态链接库 |
-static | 指定生成静态链接库 |
-fPIC | 表示编译为位置独立的代码,用于编译共享库。目标文件需要创建成位置无关码, 念上就是在可执行程序装载它们的时候,它们可以放在可执行程序的内存里的任何地方 |
-L. | 表示要连接的库所在的目录 |
-l | 指定链接时需要的动态库。编译器查找动态连接库时有隐含的命名规则,即在给出的名字前面加上lib,后面加上.a/.so来确定库的名称 |
-Wall | 生成所有警告信息 |
-ggdb | 此选项将尽可能的生成gdb 的可以使用的调试信息 |
-g | 编译器在编译的时候产生调试信息 |
-c | 只激活预处理、编译和汇编,也就是把程序做成目标文件(.o文件) |
-Wl,options | 把参数(options)传递给链接器ld 。如果options 中间有逗号,就将options分成多个选项,然后传递给链接程序 |
LD_LIBRARY_PATH | 这个环境变量指示动态连接器可以装载动态库的路径,如果有 root 权限的话,可以修改/etc/ld.so.conf 文件,然后调用 /sbin/ldconfig 来达到;如果没有 root 权限,只能采用输出 LD_LIBRARY_PATH 的方法 |
ldd | 列出动态库依赖关系。在实际linux开发与调试中, 要经常查看动态库依赖关系 |
调用动态库的时候有几个问题会经常碰到,有时,明明已经将库的头文件所在目录 通过 “-I”include 进来了,库所在文件通过 “-L”参数引导,并指定了“-l”的库名,但通过 ldd 命令察看时,就是找不到指定链接的 so 文件,这时要作的就是通过修改LD_LIBRARY_PATH
或者/etc/ld.so.conf
文件来指定动态库的目录。
1)编写一个主程序文件main.c
和两个子程序文件 sub1.c
,sub2.c
,
要求:子程序sub1.c 包含一个算术运算函数 float x2x(int a,int b),此函数功能为对两个输入整型参数做某个运算,将结果做浮点数返回;再扩展写一个x2y函数(功能自定);
main函数代码将调用x2x和x2y ;返回结果printf出来。
2)将这3个函数分别写成单独的3个 .c文件,并用gcc分别编译为3个.o 目标文件;
将x2x、x2y目标文件用 ar工具生成1个 .a 静态库文件
, 然后用 gcc将 main函数的目标文件与此静态库文件进行链接,生成最终的可执行程序,记录文件的大小。
3)将x2x、x2y目标文件用 ar工具生成1个 .so 动态库文件
, 然后用 gcc将 main函数的目标文件与此动态库文件进行链接,生成最终的可执行程序,记录文件的大小,并与之前做对比。
创建一个目录,用来保存此次练习的文件
mkdir test3
cd test3
利用vim文本编辑器编辑需要的文件sub1.c,sub2.c,sub.h,main.c,代码如下
/*sub1.c*/ float x2x(int x,int y) { float r; r=x+y; return r; }
/*sub2.c*/ float x2x(int x,int y) { float r; r=x*y; return r; }
/*sub.h*/ #ifndef SUB_H #define SUB_H float x2x(int x,int y); float x2y(int x,int y); #endif
/*main.c*/ #include<stdio.h> #include"sub.h" int main() { int x,y; x=2; y=3; printf("x+y=%f\n",x2x(x,y)); printf("x*y=%f\n",x2y(x,y)); return 0; }
在这里,只是简单设置sub1.c函数为加法运算,sub2.c函数为乘法运算,main主函数调用并输出两个运算结果
键入以下命令将三个.c文件编译成.o文件
gcc -c sub1.c
gcc -c sub2.c
gcc -c main.c
ls
可以发现生成了sub1.o,sub2.o,main.o文件
将两个目标文件用ar工具生成.a静态库
ar -crv libsub.a sub1.o sub2.o
在程序中使用静态库
gcc -o sub main.c -L. -lsub
./sub
之后即可得到运行结果
整体流程操作如下图所示
至此,main函数的目标文件与静态库的链接,以及最终可执行程序生成完毕。
键入ll
用来查看静态库生成文件大小,这一点我们将在后面进行比较。
同样我们创建动态库文件libsub.so,并使用该动态库
gcc -shared -fPIC -o libsub.so sub1.o sub2.o
gcc -o sub main.c -L. -lmyhello
./sub
果不其然,又报错了,同样,我们仍使用root
重新进行操作。
整体操作流程如下图所示
至此,main函数的目标文件与动态库的链接,以及最终可执行程序生成完毕。
键入ll
用来查看静态库生成文件大小,这一点我们将在后面进行比较。
静态库
动态库
在这里我们发现,静态库生成文件比动态库小得多。
但是在前文第一点中,我们提到,静态库占用内存空间较大,存在浪费,而这里发现其生成文件较动态库其实并不大,也许正是因为其文件小才能去占用和浪费空间吧(笑)。
通过本次gcc生成静态库和动态库的练习,熟悉了生成静态库和动态库的基本操作,以及相关概念,整体流程也在文章中得以总结。
受益良多。