linux上静态库和动态库的编译和使用(附外部符号错误浅谈)
主要参考博客gcc创建和使用静态库和动态库
对于熟悉windows的同学,linux上的静态库.a相当于win的.lib,动态库.so相当于win的.dll.
首先简要地解释下这两种函数库的区别,参考《Linux程序设计》
1. 静态库也被称为归档文件(archive,因此创建命令是ar),编译器和链接器负责将程序代码和静态库结合在一起组成单独的可执行文件;
但是缺点是许多应用程序同时运行并使用来自同一个静态库的函数时,内存中就会有一个函数的多份副本,而且程序文件自身也有多份同样的副本,这将消耗大量的内存和磁盘空间。
2. 动态库,也称共享库(因此创建命令包含share)。可执行文件不会包含动态库的函数代码,而是引用运行时可访问的共享代码,函数引用被解析并产生对动态库的调用时,动态库才会被加载到内存中。因此系统可以只保留动态库的一份副本,并且当函数功能需要改变时,只需要重新编译生成动态库,而不用重新编译整个源程序。
现在直接进入重点,贴代码。首先我创建了三个文件hello.h hello.c main.c,其中前两个是函数hello()的头文件和源文件,main.c则是调用hello()函数。代码如下
/************************************************************************* > File Name: hello.h ************************************************************************/ #ifndef _HELLO_H_ #define _HELLO_H_ void hello(); #endif
/************************************************************************* > File Name: hello.c ************************************************************************/ #include <stdio.h> #include "hello.h" void hello() { printf("hello world!\n"); }
/************************************************************************* > File Name: main.c ************************************************************************/ #include "hello.h" int main(int argc, char** argv) { hello(); return 0; }
目录组织如下(这里使用了tree,我是Ubuntu系统,命令apt-get install tree即可安装)
两个sh文件分别是使用动态库和静态库的shell文件,把命令行整合到一起,代码如下
######################################################################### # File Name: static-compile.sh ######################################################################### #!/bin/bash cd ./lib gcc -c -I../include hello.c # 生成hello.o ar rc libhello.a hello.o # 生成libhello.a cd ../src gcc main.c -I../include -L../lib -lhello -o main # 生成main cd ../ # 回到根目录
首先进入lib目录把自定义函数库的源文件hello.c进行编译生成目标文件hello.o。然后用ar rc(ar是归档,rc分别代表replace和create,即若已存在则替换、创建新文件)生成静态库libhello.a。
然后进入src目录,-I后面紧接着(没有空格)头文件目录路径(I代表include),-L后面紧接着(没有空格)库文件目录路径(L代表lib)
######################################################################### # File Name: shared-compile.sh ######################################################################### #!/bin/bash cd ./lib # 生成动态库libhello.so gcc hello.c -I../include -fpic -shared -o libhello.so # 生成可执行文件 cd ../src mv ../lib/libhello.so ./ cp ./libhello.so /lib # 将动态库复制到/lib文件夹 gcc main.c -I../include -L../lib -lhello -o main cd ../ # 回到根目录
对于动态库来说,生成命令多了-fpic和-shared。PIC代表Position-Independent Code,与位置无关,也就是使用的都是相对路径和绝对路径。shared代表共享。
而在使用动态库之前,需要把.so复制到/lib文件夹(或者设置环境变量,见我文章开头引用的博客)。
使用动态库的命令除了-I和-L外,还有个-l(小写的L),后面接着的是hello。
——这是因为我的动态库命名为libhello.so,使用-l的话会忽略前面的lib和后面的后缀名。
分别在linux下运行static-compile.sh和shared-compile.sh,效果如下
由于动态库没有把函数库的代码加入到可执行文件中,所以可以看出使用动态库链接出的程序大小偏小(8592<8664)
当然,生成程序之后,.a、.so(非/lib目录下的)文件都可以删掉,程序一样能运行。
最后谈下这两者的实际应用,就以Visual C++编程来谈吧。
很多时候会出现unsolved external symbol(未解决的外部符号)错误,如果是用其他人的库,很有可能就是忘记在菜单设置-链接器->输入->附加依赖项中加入需要的.lib文件(也就是静态库)。(比如使用winsock2.h的一些库函数时,没有#pragma comment(lib, "Ws2_32.lib"))把Ws2_32.lib静态库加载进去的话,函数就只有头文件中的声明,而缺少了库文件中的定义。
而如果是忘记把.dll路径(往往是bin文件夹)添加到环境变量中(PS:我的dynamic-compile.sh对应windows相当于是把dll放到了system32目录下),在编译的时候不会出错,而是Debug运行的时候会报错。
这就是静态库和动态库的显著区别,静态库是编译期间由链接器通过include目录找到并链接到到可执行文件中,而动态库则是运行期间动态调用,只有运行时找不到对应动态库才会报错。
说回外部符号错误,如果是自己写的,很有可能就是函数只编译了,没有定义。比如f.h中写的是void f(); 结果对应的f.cpp写的是void f2() {}看起来不可能出现这种错误,实际上由于C++重载某种意义上是改了函数名(而且还改得很长),这样的错误对新手来说很常见。
还有个典型的例子,就是C++调用C函数,报错如下
main.obj : error LNK2019: unresolved external symbol "void __cdecl f(void)" (?f@@YAXXZ) referenced in function _main
// hello.h #pragma once void f();
// hello.c #include "hello.h" void f() { }
// main.cpp #include "hello.h" int main() { f(); return 0; }
因为C++眼中,void f(void);其实是void f@@YAXXZ(void);(f后面的取决于编译器),而C眼中,f就是f。一般需要使用extern "C"来使用,或者直接把hello.c改后缀为hello.cpp。