静态链接库与动态链接库详解
转载:
分类: c、vc、cpp |
在windows下一般可以看到后缀为dll和后缀为lib的文件,但这两种文件可以分为三种库,分别是动态链接库(Dynamic-Link Libraries),目标库(Object Libraries)和导入库(Import Libraries),下面一一解释这三种库。
目标库(Object Libraries)
目标库又叫静态链接库,是扩展名为.LIB的文件,包括了用户程序要用到 的各种函数。它在用户程序进行链接时,“静态链接”到可执行程序文件当中。例如,在VC++中最常使用到的C运行时目标库文件就是LIBC.LIB。在链 接应用程序时常使用所谓“静态链接”的方法,即将各个目标文件(.obj)、运行时函数库(.lib)以及已编译的资源文件(.res)链接到一起,形成 一个可执行文件(.exe)。使用静态链接时,可执行文件需要使用的各种函数和资源都已包含到文件中。这样做的缺点是对于多个程序都使用的相同函数和资源 要重复链接到exe文件中,使程序变大、占用内存增加。
导入库(Import Libraries)
导入库是一种特殊形式的目标库文件形式。和目标库文件一样,导入库文件的扩展名也是.LIB,也是在用户程序被链接时,被“静态链接”到可执行文件当中。但是不同的是,导入库文件中并不包含有程序代码。相应的,它包含了相关的链接信息,帮助应用程序在可执行文件中建立起正确的对应于动态链接库的重定向表。比如KERNEL32.LIB、USER32.LIB和GDI32.LIB就是我们常用到的导入库,通过它们,我们就可以调用Windows提供的函数了。 如果我们在程序中使用到了Rectangle这个函数,GDI32.LIB就可以告诉链接器,这个函数在GDI32.DLL动态链接库文件中。这样,当用 户程序运行时,它就知道“动态链接”到GDI32.DLL模块中以使用这个函数。其实说白了导入库就是一个索引,一个dll动态链接库的索引表,这是我的 理解。
动态链接库(Dynamic-Link Libraries)
“动态链接”是将一些公用的函数或资源组织成动态链接库文件(.dll),当某个需要使用dll中的函数或资源的程序启动时(准确的说是初始化时),系统将该 dll映射到调用进程的虚拟地址空间、增加该dll的引用计数值,然后当实际使用到该dll时操作系统就将该dll载入内存;如果使用该dll的所有程序 都已结束,则系统将该库从内存中移除。使用同一dll的各个进程在运行时共享dll的代码,但是对于dll中的数据则各有一份拷贝(当然也有在dll中共享数据的方法)。 动态链接库中可以定义两种函数:输出函数和内部函数。输出函数可以被其他模块调用,内部函数只能被动态链接库本身调用。动态链接库也可以输出数据,但这些数据通常只被它自己的函数所使用。
如我们所知,Windows程序都是一些可执行文件,它们可以创建并显示一个或多个窗体,使用消息循环来接收用户的输入。但是动态链接库并不能直接被执 行,它们一般也不会接收消息。它们只是一些包含着函数的独立文件,这些函数可以被Windows程序或者其它DLL调用以完成某项任务。
“动态链接”是指Windows程序在运行时才把自己需要存在于某个库中的函数链接进来。“静态链接”是指Windows程序在编译阶段就把各种对象模块(.OBJ)、运行时库(.LIB)和资源文件(.RES)链接到一起以创建一个可执行文件(.EXE)。
DERNAL32.DLL,USER32.DLL,GDI32.DLL,各种驱动程序如KEYBOARD.DRV,SYSTEM.DRV和MOUSE.DRV,显卡和打印机驱动程序等都是动态链接库。这些库可以被所有的Windows程序共同使用。
有某些动态链接库(如字体文件)称为“resource-only”。它们只包括数据,而不包括代码。因此,动态链接库的目的之一就是为许多不同的程序提供 函数和资源。在传统的操作系统里,用户程序在运行时只能调用操作系统自身的某些函数。而在Windows操作系统下,模块或程序调用另一个模块中的函数来 执行是一种非常普遍的操作。因此,从某种角度看,对DLL进行编程,其实是在对Windows操作系统作扩展,也可以看作是在对用户程序作扩展。
动态链接库模块可以有其它的扩展名,但是标准的扩展名是.DLL。只有具有标准扩展句的动态链接库模块才可以被Windows自动加载。而如果是其它扩展名的动态链接库模块,程序必须使用LoadLibrary或者LoadLibraryEx函数来显示加载。
我们可以发现,在大型的应用软件中,会常常使用到动态链接库技术。举个例子,假如我们要写一个大型的应用软件,其中包括了多个程序。我们可以发现很多程 可能都会使用到一些同样的通用的函数。我们可以把这些通用的函数放到某个目标库文件中(.LIB),然后在链接是把它加到每个程序中进行静态链接。但是 这是一种非常浪费的方法,因为每个程序模块中都会包括这些通用函数的独立拷贝。另外,如果我们要改变库文件中的某个函数,就必须把所有使用到这个函数的程 序都重新编译一遍。但是,如果我们使用动态链接库的技术,把所有这些通用函数都放到一个动态链接库文件当中,我们就可以解决以上提到的各种问题。首先,动 态链接库在硬盘上只保留一个拷贝,程序只是在运行时才会调用其中使用到的函数,这样我们就可以节省大量的程序存储和运行空间。其次,如果要修改某个通用函 数时,只要调用接口没有改变,只是改变它的实现方法,那么我们就不必对每个用到它的程序都进行重新编译,而只要把动态链接库模块重新编译一遍就可以了。
动态链接库模块也可以作为一个单独的产品来发布。这样程序开发人员就可以使用第三方的模块来开发自己的应用程序,提高了程序的复用程序,也节省了大量的时间和精力。
目标库和导入库都是在程序开发过程中才使用到的,而动态链接库是在程序运行时才使用的。在程序运行时,相应的动态链接库文件必须已经保存在硬盘上了。另 外,如果要使用动态链接库文件,该文件必须要保存在同.EXE文件同一个目录下,或者保存在当前目录、Windows系统目录、Windows目录或环境 变量中PATH参数指定的目录下。程序也是按照这个顺序来搜寻它需要的动态链接库文件的。
创建静态链接库(Lib)
创建静态链接库比较简单,创建win32控制台程序,选择静态库,这里我没有选择上预编译头。生成工程以后就像定义一般的函数般,定义放在头文件,然后实现放在cpp文件里头,直接build就出来一个静态的lib了,发布时附上头文件给使用者就可以。
创建动态链接库(DLL)
用SDK创建一个简单的dll文件
在 VC++中选择新建一个Win32 Dynamic-Link Library。需要建立一个c/c++ head file和一个c/c++ source file并加入工程。头文件中内容为输出函数的声明,源文件中内容为DllMain函数和输出函数的定义。下面是一个最简单的例子。
头文件代码如下:
#define TEST_CREATE_DLL_API __declspec(dllexport)
#else
#define TEST_CREATE_DLL_API __declspec(dllimport)
#endif
// This class is exported from the test_create_dll.dll
class TEST_CREATE_DLL_API Ctest_create_dll {
public:
Ctest_create_dll(void);
// TODO: add your methods here.
};
extern TEST_CREATE_DLL_API int ntest_create_dll;
TEST_CREATE_DLL_API int fntest_create_dll(void);
在创建工程的时候TEST_CREATE_DLL_EXPORTS就已经在预定义处定义过,生成导出dll。
头 文件预处理中的__declspec是微软增加的“C扩展类存储属性”(C Extended Storage-Class Attributes),它指明一个给出的实例被存储为一种微软特定的类存储属性,可以为thread,naked,dllimport或 dllexport. [MSDN原文:The extended attribute syntax for specifying storage-class information uses the __declspec keyword, which specifies that an instance of a given type is to be stored with a Microsoft-specific storage-class attribute (thread, naked, dllimport, or dllexport).] 输出函数必须指明为CALLBACK。 DllMain是dll的入口点函数。也可以不写它。DllMain必须返回TRUE,否则系统将终止程序并弹出一个“启动程序时出错”对话框。 编译链接后,得到动态链接库文件dlldemo.dll和输入库文件dlldemo.lib。
_declspec(dllexport)
声明一个导出函数,是说这个函数要从本DLL导出。我要给别人用。一般用于dll中 。
省掉在DEF文件中手工定义导出哪些函数的一个方法。当然,如果你的DLL里全是C++的类的话,你无法在DEF里指定导出的函数,只能用__declspec(dllexport)导出类。
__declspec(dllimport)
声明一个导入函数,是说这个函数是从别的DLL导入。我要用。一般用于使用某个dll的exe中 。
不使用 __declspec(dllimport) 也能正确编译代码,但使用 __declspec(dllimport) 使编译器可以生成更好的代码。编译器之所以能够生成更好的代码,是因为它可以确定函数是否存在于 DLL 中,这使得编译器可以生成跳过间接寻址级别的代码,而这些代码通常会出现在跨 DLL 边界的函数调用中。但是,必须使用 __declspec(dllimport) 才能导入 DLL 中使用的变量。
相信写WIN32程序的人,做过DLL,都会很清楚__declspec(dllexport)的作用,它就是为了省掉在DEF文件中手工定义导出 哪些函数的一个方法。当然,如果你的DLL里全是C++的类的话,你无法在DEF里指定导出的函数,只能用__declspec(dllexport)导 出类。但是,MSDN文档里面,对于__declspec(dllimport)的说明让人感觉有点奇怪,先来看看MSDN里面是怎么说的:
不使用 __declspec(dllimport) 也能正确编译代码,但使用 __declspec(dllimport) 使编译器可以生成更好的代码。编译器之所以能够生成更好的代码,是因为它可以确定函数是否存在于 DLL 中,这使得编译器可以生成跳过间接寻址级别的代码,而这些代码通常会出现在跨 DLL 边界的函数调用中。但是,必须使用 __declspec(dllimport) 才能导入 DLL 中使用的变量。
extern "C"
指示编译器用C语言方法给函数命名。
在制作DLL导出函数时由于C++存在函数重载,因此__declspec(dllexport) function(int,int) 在DLL会被decorate,例如被decorate成为function_int_int,而且不同的编译器decorate的方法不同,造成了在用 GetProcAddress取得function地址时的不便,使用extern "C"时,上述的decorate不会发生,因为C没有函数重载,但如此一来被extern"C"修饰的函数,就不具备重载能力,可以说extern 和extern "C"不是一回事。
C++编译器在生成DLL时,会对导出的函数进行名字改编,并且不同的编译器使用的改变规则不一样,因此改编后的名字会不一样。这样,如果利用不同 的编译器分别生成DLL和访问该DLL的客户端代码程序的话,后者在访问该DLL的导出函数时会出现问题。为了实现通用性,需要加上限定符:extern “C”。
但是利用限定符extern “C”可以解决C++和C之间相互调用时函数命名的问题,但是这种方法有一个缺陷,就是不能用于导出一个类的成员函数,只能用于导出全局函数。 LoadLibrary导入的函数名,对于非改编的函数,可以写函数名;对于改编的函数,就必须吧@和号码都写上,一样可以加载成功,可以试试看。
解决警告 inconsistent dll linkage
inconsistent dll linkage警告是写dll时常遇到的一个问题,解决此警告的方法如下:
一般PREDLL_API工程依赖于是否定义了MYDLL_EXPORTS来决定宏展开为__declspec(dllexport)还是 __declspec(dllimport)。展开为__declspec(dllexport)是DLL编译时的需要,通知编译器该函数是需要导出供外 部调用的。展开为__declspec(dllimport)是给调用者用的,通知编译器,该函数是个外部导入函数。
对于工程设置里面的预定义宏,是最早被编译器看到的。所以当编译器编译DLL工程中的MYDLL.cpp时,因为看到前面有工程设置有定义MYDLL_EXPORTS,所以就把PREDLL_API展开为__declspec(dllexport)了。
这样做的目的是为了让DLL和调用者共用同一个h文件,在DLL项目中,定义MYDLL_EXPORTS,PREDLL_API就是导出;在调用该DLL的项目中,不定义MYDLL_EXPORTS,PREDLL_API就是导入。
使用静态链接库(Lib)
使用静态链接库需要库的开发者提供库的头文件以及lib文件,一般来说lib文件都比较大(相对导入库来说),静态链接库是将全部指令都包含入调用程序生成的EXE文件中,并不存在“导出某个函数提供给用户使用”的情况,就是要么全要,要么都不要。
使用动态链接库(DLL)
方法一: load-time dynamic linking (隐式调用)(又名:静态绑定)
在要调用dll的应用程序链接时,将dll的输入库文件(import library,.lib文件)包含进去。具体的做法是在源文件开头加一句#include ,然后就可以在源文件中调用dlldemo.dll中的输出文件了,
#pragma comment(lib, "***.lib") //通知编译器DLL的.lib文件所在路径及文件名,也可以不采用该语句,在属性栏——输入——附加依赖项处添加对应的lib就可以编译链接应用程序了。
extern "C" __declspec(dllimport) foo(); //声明导入函数
方法二: run-time dynamic linking (显式调用)(又名:动态绑定)
不必在链接时包含输入库文件,而是在源程序中使用LoadLibrary或LoadLibraryEx动态的载入dll。
主要步骤为(以demodll.dll为例):
1) typedef函数原型和定义函数指针。
typedef void (CALLBACK* DllFooType)(void) ;
DllFooType pfnDllFoo = NULL ;
2) 使用LoadLibrary载入dll,并保存dll实例句柄
HINSTANCE dllHandle = NULL ;
...
dllHandle = LoadLibrary(TEXT("dlldemo.dll"));
3) 使用GetProcAddress得到dll中函数的指针
pfnDllFoo = (DllFooType)GetProcAddress(dllHandle,TEXT("DllFoo")) ;
注意从GetProcAddress返回的指针必须转型为特定类型的函数指针。
4)检验函数指针,如果不为空则可调用该函数
if(pfnDllFoo!=NULL)
DllFoo() ;
5)使用FreeLibrary卸载dll
FreeLibrary(dllHandle) ;
动态链接库(DLL)的优点
→节约内存;
→使应用程序“变瘦”;
→可单独修改动态链接库而不必与应用程序重新链接;
→可方便实现多语言联合编程(比如用VC++写个dll,然后在VB中调用);
→可将资源打包;
→可在应用程序间共享内存
→......