背景
我写了一个矩阵的基本运算库,头文件matrix.h
中是class matrix
成员变量和方法的声明,方法的具体实现放在matrix.cpp
中。在main.cpp
中#include <matrix.h>
调用库文件。
以往要编译这种文件的时候,往往都是g++ matrix.cpp main.cpp -o main
直接生成一个main
可执行文件。如果项目还有还多其他的依赖呢?继续把全部代码打包成一个很大的可执行文件吗?如果日后很多程序都复用这个模块,是否会存储和加载冗余的代码?
静态链接与动态链接
首先我们从C++的编译过程入手,一般一个C++程序被编译的过程分几步:预编译,编译,汇编,链接。
g++ -c matrix.cpp -o matrix.o
通过制定-c
参数,告诉编译器只编译matrix.cpp
,但不要链接,而生成的matrix.o
文件称为对象文件(object file)(如果不指定-o
输出文件,默认输出cpp
文件同名的*.o
文件)。这条命令完成了链接之前的全部过程:编译和汇编。接下来可以将汇编生成的二进制对象文件matrix.o
和库一起链接,打包成可执行文件main
:
msvc下对象文件为*.obj
g++ main.cpp matrix.o -o main |
或者:
g++ -c main.cpp -o main.o |
在不指定-c
参数的时候,g++
会自动识别参数中的文件类型并可执行文件。
这种过程生成和直接g++ main.cpp matrix.cpp -o main
的构建过程等价。在编译期链接阶段生成的对象文件(.o)需要和相关函数库链接,形成一个可执行文件。程序在执行时,直接从该文件加载相关库,这种链接方式叫做*静态链接。
如果把一些库函数的链接过程推迟到运行时进行,这就是动态链接,参与链接的库被成为动态链接库(dynamic link library),在Windows下动态链接库文件表现为*.dll
的形式,在Linux下只是*.so
静态链接库
静态连接过程中使用的相关的函数库被称作是静态链接库(static library),在Windows下常常以*.lib
文件出现,在Linux下是*.a
文件。
静态链接库同是经过汇编后的二进制文件,内容上和对象文件相似,可以看成是一组目标文件(.o / .obj文件)的压缩打包后的集合。
同上述对象文件连接过程一样,静态链接库对函数库的链接是在编译期完成的。编译完成后,生成的程序便于静态链接库没有关系,因此移植方便。
Linux静态链接库
Linux静态链接库的文件命名必须是lib[libname].a
,Linux中创建一个静态链接库过程如下:
- 将编译代码文件编译成对象文件:
g++ -c matrix.cpp -o matrix.o
- 用ar工具打包成静态链接库
matrix.a
:ar -crv libmatrix.a matrix.o
使用静态链接库编译,需要保证libmatrix.a
和matrix.h
头文件在当前目录下:g++ main.cpp -L . -lmatrix -o main
生成的main
可执行文件已经打包了libmatrix.a
数据和程序,可以被移植后单独运行。
Windows静态链接库
Windows下基于VS的开发模式下,要创建一个静态链接库只需要新建一个Static Library项目,然后在头文件#include "stdafx.h"
,他用于生成一个预编译头文件project.pch
和预编译类型文件stdafx.obj
。在编译完成后,就会生成对应的*.lib
文件了。
使用静态链接库的时候,在头部加上:#pragma comment(lib,"matrix.lib")
动态链接库
在使用动态库的时候,往往提供两个文件:一个引入库(.lib)文件和一个DLL (.dll) 文件。虽然引入库的后缀名也是“lib”,但是,动态库的引入库文件和静态库文件有着本质上的区别,对一个DLL来说,其引入库文件(.lib)包含该DLL导出的函数和变量的符号名,而.dll文件包含该DLL实际的函数和数据。在使用动态库的情况下,在编译链接可执行文件时,只需要链接该DLL的引入库文件,该DLL中的函数代码和数据并不复制到可执行文件中,直到可执行程序运行时,才去加载所需的DLL,将该DLL映射到进程的地址空间中,然后访问DLL中导出的函数。这时,在发布产品时,除了发布可执行文件以外,同时还要发布该程序将要调用的动态链接库。
Windows API中所有的函数都包含在DLL中,其中有3个最重要的DLL:
- Kernel32.dll:它包含那些用于管理内存、进程和线程的函数,例如CreateThread函数;
- User32.dll:它包含那些用于执行用户界面任务(如窗口的创建和消息的传送)的函数,例如 CreateWindow 函数;
- GDI32.dll:它包含那些用于画图和显示文本的函数。
使用动态链接库的优点:
- 可以用多种程序编写动态链接库:
动态链接库文件是系统相关的,可以用熟悉的语言开发动态链接库,然后在不同的语言开发的应用程序种调用它。于是可以充分发挥各种语言的特性,针对特定模块功能选用不同的语言。
- 节约磁盘和内存空间
如果多个程序需要使用同样的功能,可以将共同的功能模块以动态链接库的形式提供,所以同样的程序在磁盘上只需要存在一份,节约了磁盘空间。另外,如果多个进程使用同一份动态连接库,操作系统将保证该份动态链接库文件只需要放入内存一次,他的内存空间可以被多个应用程序共享,也节约了内存空间。
- 实现资源共享
动态链接库可以包含对话框模板、字符串、图标和位图等多种资源,多个应用程序可以使用动态链接库来共享这些资源。在实际工作中,可以编写一个纯资源的动态链接库,供其他应用程序访问。因此动态链接库也被称为共享库。
- 有助于实现应用程序的本地化
如果产品需要提供多语言版本,那么就可以使用DLL来支持多语言。可以为每种语言创建一个只支持这种语言的动态链接库。
提供二次开发的平台
在销售产品的同时,可以采用DLL的形式提供一个二次开发的平台,让用户可以利用该DLL调用其中实现的功能,编写符合自己业务需要的产品,从而实现二次开发。增强产品的功能
在发布产品时,可以发布产品功能实现的动态链接库规范,让其他公司或个人遵照这个规范开发自己的DLL,以取代产品原有的DLL,让产品调用新的DLL,从而实现功能 的增强。在实际工作中,我们看到许多产品都提供了界面插件功能,允许用户动态地更换程序的界面,这就可以通过更换界面DLL来实现。
- 简化项目管理
在一个大型项目开发中,通常都是由多个项目小组同时开发,如果采用串行开发,则效率是非常低的。我们可以将项目细分,将不同功能交由各项目小组以多个DLL的方式实现,这样,各个项目小组就可以同时进行开发了。
- 方便程序更新
如果项目依赖的静态链接库更新了,则主程序需要整个重新更新。
- 支持手动载入程序
一般的静态库加载代码段都是操作系统自行完成的,无法人为控制。如果一些项目特别大,就需要有选择的载入一些库,而动态链接库允许开发者在运行时显示的加载,调用和释放某一部分代码。
Windows动态链接库
Windows下可以用VS创建一个Dynamic Link Library项目来构建一个动态链接库。在Windows系统下的执行文件格式是PE格式,动态库需要一个DllMain
函数做出初始化的入口,通常在导出函数的声明时需要有_declspec(dllexport)
关键字,表明函数将输出为动态链接库。编译后生成*.lib
,*.dll
等文件。
在使用动态库时,往往提供两个文件:一个导入库(*.lib
,非必须) 和一个(*.dll
)文件。
导入库和静态库本质上的区别:静态库本身就包含了实际执行代码和地址符号表等数据。而对于导入库而言,其实际的执行代码位于动态库中,导入库只包含了地址符号表等,确保程序找到对应函数的一些基本地址信息。
在调用动态链接库文件*.dll
的时候,需要引入库的头文件(.h),导入库文件(.lib),并加上#pragma comment(lib,"matrix.lib")
,还需要把*.dll
文件放在可执行文件的生成目录。
Linux动态链接库
Linux下gcc编译的执行文件默认是ELF格式,不需要初始化入口,亦不需要函数做特别的声明,编写比较方便:g++ -shared -fPIC main.cpp -o libmatrix.so
通过指定-shared
参数,编译matrix.cpp
生成libmatrix.so
动态链接库文件。其中-fPIC
参数是为了生成启示地址无关的动态库。详细原理见这篇文章:关于 gcc/g++编译选项: -fPIC 功能的解释
动态链接库的隐式调用和显式调用
动态链接库调用分为隐式调用和显式调用。隐式调用是运行时执行到某个函数,由操作系统去寻找并链接库;显式调用是开发者自己声明库文件的位置和函数名,手动加载代码段,控制代码执行和代码块的释放。
由于对Windows平台C++的开发没有经验,Windows相关的DLL使用之后再整理
隐式调用(Linux)
我们先就开篇的背景问题,用动态链接库写个完整的例子:
g++ -shared -fPIC -o libmatrix.so matrix.cpp
通过指定-shared
参数,编译matrix.cpp
生成libmatrix.so
动态链接库文件,接着用matrix g++ -o main main.cpp -L . -lmatrix
生成可执行文件main
,其中-L .
指定动态链接库目录为当前目录,-lmatrix
指明需要动态链接的库,编译器会自动去寻找libmatrix.so
。
编译链接成功后,但执行可执行程序main
报错:./main: error while loading shared libraries: libmatrix.so: cannot open shared object file: No such file or directory
提示链接器找不到libmatrix.so
,用ldd main
检查main
种动态链接库的完整性:$ ldd main
linux-vdso.so.1 (0x00007fffd9366000)
libmatrix.so => not found
libstdc++.so.6 => /usr/lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f611ccc0000)
libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f611cca0000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f611caa0000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f611c951000)
/lib64/ld-linux-x86-64.so.2 (0x00007f611ceca000)
果然是运行时找不到libmatrix.so
,这里有几种方式解决动态链接库的路径问题:
- 设置LD_LIBRARY_PATH环境变量:
export LD_LIBRARY_PATH=$(pwd)
- 向
/etc/ld.so.conf
中添加一行路径,然后执行sudo ldconfig
- 将
libmatrix.so
拷贝到默认动态链接库目录/usr/local/lib
修复完路径后,再次执行main
就可以正常运行了。上述这样的一个过程就是隐式调用,隐式调用的优点是方便,实现简单。缺点是灵活性不足,如果一个程序模块特别多,启动时库全部加载,会严重拖慢启动时间。而且一些方法之后很少时间会用到,他们大部分时间可以被主动释放掉,从而减少内存占用。