一、动态库和静态库
动态库和静态库各自有自己的优势,动态库可以减小应用程序的体积,动态加载,特别是共享库还节省内存,静态库加载快执行速度也稍微快一些,但相对来说就是可执行文件的体积大,修改后需要重新编译。说回来还是空间时间效率等的平衡。
对第三方开放的重要的库,动态库居多,一来升级方便,再提供一个新库即可;如果提供一个框架库,就需要静态库或者说又提供静态库同时提供动态库,比如常见的C和C++的库,都提供了静态库同时也提供了动态库。不过一般在编译时(没有强制指定),默认都是链接的动态库,如果没有链接到动态库则会选择静态库。
二、互相调用
在学习完动态库和静态库之后,就出现了一个问题。在前面提到过,如何掌握判断何时采用动态库,何时采用静态库。如果根据实际情况,做出了选择,那么在实际情况中很可能会发现,可能算法用的静态库,逻辑用的动态库,而业务根据场景又有动态库又有静态库。同学们可能说,怎么这么乱呢?
确实是,实际的工作环境中,不但有各种选择产生的结果,还有使用友商的库,还有历史遗留下来的库。这下就明白为什么每次微软的升级有多么痛苦了吧。各种库的调用方式和接口的命名方式,眼花缭乱。正如前面所讲,这时候儿要跳出来看,其实不外乎两种情况,动态库调静态库,或者反过来。当然,也有乱调的,“年轻人,不讲武德!”。那其实也是上面的逻辑,认真分析就可以了。
三、例程
代码走一波:
这里面有三种形式:
1、静态调用动态库
//dy.h
int Add(int c);
//dy.cpp
#include "dy.h"
int Add(int c)
{
return c+ c;
}
//sa.h
int GetData(int c);
//sa.cpp
#include "dy.h"
#include <iostream>
int GetData(int c)
{
int d = 0;
d = Add(c);
std::cout<<"d value is:"<<d<<std::endl;
return d;
}
//main.cpp
#include "sa.h"
#include <iostream>
int main()
{
int ret = GetData(10);
std::cout<<"call static-dy value:"<<ret<<std::endl;
return 0;
}
编译:
s2d$g++ -fPIC -shared -o libdy.so dy.cpp ---生成动态库
s2d$ g++ -c sa.cpp ---生成静态库
s2d$ ar -rcs libsa.a sa.o
/s2d$ g++ -o main main.cpp -L. -ldy -lsa ---编译运行程序
s2d$LD_LIBRARY_PATH=. ./main ---临时指定库路径 执行
执行结果:
d value is:20
call static-dy value:20
2、动态库调用静态库
同样采用上面的代码,但是把编译的动、静态库的文件调换一下:
编译:
g++ -fPIC -shared -o libsa.so sa.cpp ---原动态库文件编译成静态库,反之同样
d2s$ g++ -c dy.cpp
d2s$ ar -rcs libdy.a dy.o
d2s$ g++ -o main main.cpp -L. -ldy -lsa
./libsa.so:对‘Add(int)’未定义的引用
collect2: 错误:ld 返回 1
咋回事?还出现了链接错误,换个位置:
g++ -o main main.cpp -L. -lsa -ldy
执行:
d2s$ LD_LIBRARY_PATH=. ./main
d value is:20
call static-dy value:20
那是不是第一次那个调换一下顺序也不行,改一下,却发现编译没有问题,执行也没有问题。原因很简单,静态库加载是全加载,都可以扫描到相关的函数符号,而动态库则不然,如果他在最后,那么他调用的符号就链接不到了。会报一个错误。这是一个需要引起注意的地方。
通过上面两种编译发现,没有使用加载静态库的编译选项-static,正如开头所讲一样,会自动寻找动态库,找不到就加载静态库。所以这种编译可以过去,那么如果既有静态库又有动态库的情况下呢:
1、指定路径
也就是说如果同时存在静态和动态库,假如必须使用静态库,可以直接指定静态库的路径和名字,仍然以上面的例子为例,将静态库再编译出一个同名的静态库来:
d2s$ g++ -fPIC -shared -o libdy.so dy.cpp
d2s$ ls
dy.cpp dy.h dy.o libdy.a libdy.so libsa.so main main.cpp sa.cpp sa.h
d2s$ g++ -o main main.cpp -L. -lsa -ldy
d2s$ ldd main
linux-vdso.so.1 (0x00007ffcb11f3000)
libsa.so => not found -------------依赖两个动态库
libdy.so => not found
libstdc++.so.6 => /usr/lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f7252332000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f7251f94000)
libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f7251d7c000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f725198b000)
d2s$ g++ -o main main.cpp -L. -lsa libdy.a
d2s$ ldd main
linux-vdso.so.1 (0x00007ffee0f46000)
libsa.so => not found ----------依赖一个动态库
libstdc++.so.6 => /usr/lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007ff34aad0000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007ff34a732000)
libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007ff34a51a000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ff34a129000)
同时再看一下文件的大小:
d2s$ ls -al ----静态链接
总用量 60
drwxrwxr-x 2 fjf fjf 4096 11月 29 11:01 .
drwxrwxr-x 4 fjf fjf 4096 11月 29 09:57 ..
-rw-rw-r-- 1 fjf fjf 1344 11月 29 10:06 libdy.a
-rwxrwxr-x 1 fjf fjf 8600 11月 29 10:03 libsa.so
-rwxrwxr-x 1 fjf fjf 9016 11月 29 11:01 main ----9016
d2s$ ls -al ---动态链接
总用量 68
drwxrwxr-x 2 fjf fjf 4096 11月 29 11:03 .
drwxrwxr-x 4 fjf fjf 4096 11月 29 09:57 ..
-rwxrwxr-x 1 fjf fjf 7616 11月 29 11:03 libdy.so
-rwxrwxr-x 1 fjf fjf 8600 11月 29 10:03 libsa.so
-rwxrwxr-x 1 fjf fjf 8952 11月 29 11:03 main ---8592
也就是说可以通过指定具体的路径和名字来操作可以达到要求的目的,但是如果路径很长,或者说多个路径,这个就有点不美观。
2、编译选项
使用-static 来链接静态库,但是这种方式的缺点是,具有传染性,其后所有的库都是使用静态库链接,如果没有提供静态库就会报错。
d2s$ g++ [-m32] -o main main.cpp -L. -lsa -static -ldy
/usr/bin/x86_64-linux-gnu-ld: 找不到 -lsa
那么可以使用如下编译选项:
d2s$ g++ -o main main.cpp -L. -Wl,-Bdynamic -lsa -Wl,-Bstatic -ldy -Wl,-Bdynamic
d2s$ ldd main
linux-vdso.so.1 (0x00007ffd845d0000)
libsa.so => not found -------------只依赖一个动态库
libstdc++.so.6 => /usr/lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007fdc1efcc000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007fdc1ec2e000)
libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007fdc1ea16000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fdc1e625000)
/lib64/ld-linux-x86-64.so.2 (0x00007fdc1f35a000)
-Wl,-Bdynamic和-Wl,-Bstatic是分别指定动态库和静态库,最后再使用一个次-Wl,-Bdynamic的目的是恢复默认,即使用动态库。[-m32] 的目的是在X64平台上编译32位库,不过可能会报一些错误,需要安装相关的X32平台上的库文件。
3、直接编译
在前面都是在程序中同时链接静态和动态库,有没有可能直接链接一个就可以呢?答案是可以的,也就是前面提到过的,在编译动态库过程中,只用-c选项编译出.o,而不直接编译出.so库文件。然后将静态库的文件 .a和.o直接编译成一个.so文件,这其实属于一种编译期的方法了。这里就不详细介绍了。有兴趣的可以查找相关资料,因为这个只有在自己可控制的情况下才可以这样做。如果别人提供了动态库和静态库之后,就没有办法这样做了。
其它的如动态库调用动态库,静态库调用静态库,可执行文件调用多个动态库或者调用多个静态库,都是很常见的,这里就不再展开了。
四、注意点
1、注意消除警告
在库的编译过程中,对于一些运行库的调用,由于C语言的一些特性,可能对未声明包含头文件时,会自动进行处理。大多数情况下,程序仍然可以正常运行,但在一些特殊情况下,可能导致程序编译链接成功后,运行时崩溃。这个
在实际工程中遇到过,一定经引起注意。这也是编码风格控制中一项,一定要消除警告。
2、版本保持一致
这个版本既包括自定义的功能版本,也包括平台和编译的位数,都要保持一致。
3、保持顺序性
即尽量保持动态调用静态,或者静态调用动态,二者的单向性,不要来回穿插。
4、尽量保持调用约定的一致性
对导出变量和符号有统一的规定,导出的方式也有统一的规定。这样能最大保证问题的减小。
5、隔离不同的库
对第三库,算法库,动静态库的调用,统一处理接口,不要各自为战。
6、注意c和c++库的互相调用
如果单纯的C和c++类型的库自己互调,没有什么问题。如果C调用C++的库,一般也会想起来了c++的改名机制,会用NM命令或者工具看一下导出的函数名。但是用c++调用C的库,这时候儿就看有没有注意了,c++仍然会改名,虽然看C库的导出名字没变。这时仍需要使用extern "C"之类的方法来解决找不到函数名的问题。
7、链接扫描顺序是从左到右
所以在使用链接库时,越是基础的库,越要放在最后。
五、总结
无论是动态库还是静态库,在实际工程出现的问题主要有几大类:
第一类就是初级选手的低级问题:
1、头文件顺序或者干脆没有包含
2、库没拷贝到指定路径
3、配置环境有问题,比如指定的环境变量错误
第二类就是一些常见的问题:
1、导出函数的约定没处理好,一个用的C形式,一个用C++形式
2、不同平台的问题,如X64,X32,ARM等
3、库的版本处理问题,这个静态库还好说一些,动态库才是真正需要引起注意的。
最后还有一类需要高度注意:
一种非常隐秘的问题,就是库之间的变量依赖问题,这个在前面说过很多次,一定要集中处理,或者干脆不依赖。因为毕竟无法强力的保证每个调用你的库的工程都是有顺序的。
总体上来说,库的编译和使用是把双刃剑,使用不好,反而起到设计相反的作用,这点一定要切记。