动态链接库
在Windows 应用程序中使用动态链接库有很多的好处。最主要的一点说是它可以使得多个应用程序共享一段代码,从而可以大幅度的降低应用程序的资源开销,同时很缩小了应用程序的最终执行代码的大小。此外,通过使用动态链接库,我们可以把一些常规的例程独立出来,有效的避免了不必要的重复开发,并且,由于应用程序使用了动态链接的方式,还可以在不需重新改写甚至编译应用程序的基础上更新应用程序的某些组件。
DLL 分三种 ,包括:非MFC DLL、静态链接到MFC的常规DLL、动态链接到MFC的常规DLL、MFC扩展DLL,
1.非MFC DLL(non-MFC DLL)内部不使用MFC,调用非MFC DLL提供的导出函数的可执行程序可以使用MFC,也可以不使用MFC。一般来说,非MFC DLL的导出函数都使用标准的C接口(standard C interface)。
2.其余三种DLL的内部都使用了MFC。顾名思义,静态链接到MFC的常规DLL(regular DLL statically linking to MFC)和动态链接到MFC的常规DLL(regular DLL dynamically linking to MFC)的区别在于一个使用的是MFC的静态链接库,而另一个使用的是MFC的DLL。这和一般的MFC应用程序的情况是很类似的。
3.MFC 扩展DLL一般用来提供派生于MFC的可重用的类,以扩展已有的MFC类库的功能。MFC扩展DLL使用MFC的动态链接版本。只有使用MFC动态链接的可执行程序(无论是EXE还是DLL)才能访问MFC扩展DLL。MFC扩展DLL的另一个有用的功能是它可以在应用程序和它所加载的MFC扩展DLL之间传递MFC和MFC派生对象的指针。在其它情况下,这样做是可能导致问题的。
如何选择所应该使用的 DLL 的类型呢? 我们可以从以下几个方面来考虑:
1.相比使用了MFC的DLL而言,非MFC DLL显得更为短小精悍。因此,如果DLL不需要使用MFC,那么使用非MFC DLL是一个很好的选择,它将显著地节省磁盘和内存空间。同时,无论应用程序是否使用了MFC,都可以调用非MFC DLL中所导出的函数。
2.如果需要创建使用了MFC的DLL,并希望MFC和非MFC应用程序都能使用所创建的DLL,那么可以选择的范围包括静态链接到MFC的常规DLL和动态链接到MFC的常规DLL。动态链接到MFC的常规DLL比较短小,因此可以节省磁盘和内存,但是,在分发动态链接到MFC的常规DLL时,必须同时分发MFC的支持DLL,如MFCx0.DLL和MSVCRT.DLL等。而使用静态链接到MFC的常规DLL则不存在这种问题。
3.如果希望在DLL中实现从MFC派生的可重用的类,或者是希望在应用程序和DLL之间传递MFC的派生对象时,必须选择MFC扩展DLL。
创建和使用动态链接库
DLL文件和EXE文件都属于可执行文件,不同的是DLL文件包括了一个导出表,导出表中给出了可以从DLL中导出的所有函数的名字。外部可执行程序只能访问包括在DLL的导出表中的函数,DLL中的其它函数是私有的,不能为外部可执行程序所访问。可以使用Visual C++提供的DUMPBIN实用程序(可以在DevStudio/VC/bin目录下找到这个工具)来查看一个DLL文件的结构。举一个例子,如果需要查看DLL文件msgbox.dll的导出表,可以在命令提示符下键入下面的命令:
1:
>dumpbin /exports msgbox.dll
运行结果如下:
1:
Microsoft (R) COFF Binary File Dumper Version 5.00.7022
2:
3:
Copyright (C) Microsoft Corp 1992-1997. All rights reserved.
4:
5:
Dump of file msgbox.dll
6:
7:
File Type: DLL
8:
9: Section contains the following Exports for MSGBOX.dll
10:
11:
0 characteristics
12:
13:
351643C3 time date stamp Mon Mar 23 19:13:07 1998
14:
15:
0.00 version
16:
17:
1 ordinal base
18:
19:
1 number of functions
20:
21:
1 number of names
22:
23:
ordinal hint name
24:
25:
1 0 MsgBox (00001000)
26:
27:
Summary
28:
29:
7000 .data
30:
31:
1000 .idata
32:
33:
2000 .rdata
34:
35:
2000 .reloc
36:
37:
17000 .text
38:
由上面的结果得知,msgbox.dll中仅包括了一个导出函数MsgBox()。
注意:
仅仅知道导出函数的名称并不足以从DLL中导出该函数。若在应用程序中使用显式链接(link explicitly),至少还应该知道导出函数的返回值的类型以及所传递给导出函数的参数的个数、顺序和类型;若使用隐含链接(link implicitly),必须有包括导出函数(或类)的定义的头文件(.H文件)和引入库(import library,.LIB文件),这些文件是由DLL的创建者所提供的。
或者用depends工具也可以,而且此工具为可视化界面,使用更方便。
从 DLL 中导出函数有两种方法:
在创建DLL时使用模块定义(module DEFinition,.DEF)文件。
在定义函数时使用关键字_declspec(dllexport)。
下面我们通过一个简单的例子来分别说明两种方法的使用。在这个例子中,我们将创建一个只包括一个函数MsgBox()的DLL,函数MsgBox()用来显示一个消息框,它和Win32 API函数MessageBox()的功能是一样,只不过在函数MsgBox()中,不需要指定消息的父窗口,而且可以缺省其它所有的参数。
(1) 使用模块定义文件
模块定义文件是一个文本文件,它包括了一系列的模块语句,这些语句用来描述DLL的各种属性,典型的,模块语句定义了DLL中所导出的函数的名称和顺序值。
在讲解模块定义文件之前,我们先创建一个Win32 Dynamic-Link Library工程。
1. 在Microsoft Developer Studio中选择File菜单下的New命令,在Projects选项卡中选择Win32 Dynamic-Link Library,并为工程取一个名字,如msgbox。单击OK后,Visual C++创建一个Win32 DLL的空白工程,必须手动的将所需要的文件添加到工程中。
2. 单击Project菜单下的Add To Project子菜单下的New命令,在Files选项卡中选择Text File,在File文本框中输入DEF文件名,如msgbox.def。
3. 双击Workspace窗口的FileView选项卡中的msgbox.def节点,在msgbox.def文件中输入下面的内容:
1:
LIBRARY MSGBOX
2:
3:
EXPORTS
4:
5:
MsgBox @1
在DEF文件中的第一条语句必须是LIBRARY语句,该语句表明该DEF文件属于一个DLL,在LIBRARY之后是DLL的名称,这个名称在链接时将放到DLL的引入库中。EXPORTS 语句下列出了DLL的所有导出函数以及它们的顺序值。函数的顺序值不是必须的,在指定导出函数的顺序值时,我们在函数名后跟上一个@符号和一个数字,该数字即导出函数的顺序值。如果在DEF中指定了顺序值,它必须不小于1,且不大于DLL中所有导出函数的数目。
注意:对于VS2005还要有以下设置,选择 工程 > 属性中的链接器,然后找到"输入"这一项. 在 "模块定义文件" 中输入 msgbox.def.
4. 下一步是向工程中添加一个头文件,它定义了DLL中的函数的返回值的类型和参数的个数、顺序和类型。
单击菜单项Project|Add To Project|New...,在Files选项卡下选择C/C++ Header File,在File文本框中指定头文件名,如msgbox.h(可以省略后缀名.h)。
在头文件中输入如下的内容:
1:
#include
2:
3: extern "C" int MsgBox(
4:
5: LPCTSTR lpText=L"虽然这个例子有一些幼稚,但它工作得非常的好!" , // 消息框的文本
6:
7: LPCTSTR lpCaption=L"一个简单的例子" , // 消息框的标题
8:
9: UINT uType=MB_OK); // 消息框的样式
请注意函数定义前的关键字extern "C",这是由于我们使用了C++语言来开发DLL,为了使C语言模块能够访问该导出函数,我们应该使用C链接来代替C++链接。否则,C++编译器将使用C++的类型安全命名和调用协议,这在使用C调用该函数时就会遇上问题。在本例中并不需要考虑到这个问题,因为我们在开发DLL和应用程序时都是使用C ++,但我们仍然强烈建议使用extern "C",以保证在使用C编写的程序调用该DLL的导出函数不会遇上麻烦。
5. 下面要做的事是向工程中添加一个C++源文件,在该文件中实现函数MsgBox()。
仿照上面的过程,单击菜单项Project|Add To Project|New...,在Files选项卡下选择C++ Source File,在File文本框中指定源文件名,如msgbox.cpp。
在msgbox.cpp文件中添加如下的代码:
1: #include "msgbox.h"
2:
3: int MsgBox(LPCTSTR lpText, LPCTSTR lpCaption, UINT uType)
4:
{
5: return MessageBox(NULL,lpText,lpCaption,uType);
6:
}
编译该工程,在Debug目录下生成文件msgbox.lib和msgbox.dll。
(2) 使用关键字_declspec(dllexport)
从DLL中导出文件的另一种方法是在定义函数时使用_declspec(dllexport)关键字。这种方法不需要使用DEF文件。
仍使用前面的例子,在工程中删除msgbox.def文件,将msgbox.h文件修改如下:
1:
#include
2: extern "C" _declspec(dllexport) int MsgBox( // 消息框的文本
3: LPCTSTR lpText=L"虽然这个例子有一些幼稚,但它工作得非常的好!" , // 消息框的标题
4: LPCTSTR lpCaption=L"一个简单的例子" , // 消息框的样式
5:
UINT uType=MB_OK);
msgbox.cpp文件并不需要做任何修改,重新编译该工程,在Debug目录下仍生成两个文件msgbox.lib和msgbox.dll。
使用__declspec(dllexport)从DLL中导出类的语法如下:
1: class __declspec(dllexport) CDemoClass
2:
{
3:
...
4:
}
5:
注意:
如果在使用__declspec(dllexport)的同时指定了调用协议关键字,则必须将__declspec(dllexport)关键字放在调用协议关键字的左边。如:int __declspec(dllexport) __cdacl MyFunc();
如何从这两种导出函数的方法中作出选择,可以从下面的几个方面考虑:
如果需要使用导出顺序值(export ordinal value),那么应该使用DEF文件来导出函数。只在使用DEF文件导出函数才能指定导出函数的顺序值。使用顺序值的一个好处是当向DLL中添加新的函数时,只要新的导出函数的顺序值大于原有的导出函数,就没有必要重新链接使用隐含链接的应用程序。相反,如果使用__declspec (dllexport)来导出函数,如果向DLL中添加了新的函数,使用隐含链接的应用程序有可以需要重新编译和链接。
使用DEF文件来导出函数,可以创建具有NONAME属性的DLL。具有NONAME属性的DLL在导出表中仅包含了导出函数的顺序值,这种类型的DLL在包括有大量的导出函数时,其文件长度要小于通常的DLL。
使用DEF文件从C++文件导出函数,应该在定义函数时使用extern "C"或者在DEF文件中指定导出函数的decorated name。否则,由于编译器所产生的decorated name是基于特定编译器的,链接到该DLL的应用程序也必须使用创建DLL的同一版本的Visual C++来编译和链接。
由于使用__declspec(dllexport)关键字导出函数不需要编写DEF文件,因此,如果编写的DLL只供自己使用,使用__declspec(dllexport)较为简单。
链接应用程序到 DLL
同样,链接应用程序到DLL也有两种方法:隐含链接、显式链接
(1) 使用隐含链接
在使用隐含链接除了需要相应的DLL文件外,还必须具备如下的条件:一个包括导出的函数或C++类的头文件、一个输入库文件(.LIB文件)
将三个文件放入程序的目录中,在程序中的项目——属性——配置属性——链接器——输入,在附加依赖项中将LIB文件名写上(也可在文件头加入#pragma comment(lib,"msgbox.lib")),包含所创建的DLL:msgbox.dll所对应的头文件msgbox.h如下:
1: extern "C" __declspec(dllimport)int MsgBox(
2: LPCTSTR lpText=L"虽然这个例子有一些幼稚,但它工作得非常的好!" ,// 消息框的文本
3: LPCTSTR lpCaption=L"一个简单的例子" , // 消息框的标题
4: UINT uType=MB_OK); // 消息框的样式
5:
需要注意的是,这个msgbox.h文件和创建DLL时所使用msgbox.h是不同的,唯一的差别在于,创建DLL时的msgbox.h中使用的是 __declspec(dllexport)关键字,而供应用程序所使用的msgbox.h中使用的是__declspec(dllimport)关键字。无论创建DLL时使用的是DEF文件还是__declspec(dllexport)关键字,均可使用__declspec(dllimport)关键字从DLL中引入函数。引入函数时也可以省略__declspec(dllimport)关键字,但是使用它可以使编译器生成效率更高的代码。
注意:
如果需要引入的是DLL中的公用数据和对象,则必须使用__declspec(dllimport)关键字。
如果DLL使用的是def文件,要删除TestDll.h文件中关键字extern "C"。
现在使用Microsoft Developer Studio创建一个Win32 Application工程,命名为tester。向工程中添加一个C++源文件,如tester.cpp。在tester.cpp文件中输入下面的代码:
1: #include "msgbox.h" // 应将msgbox.h文件拷贝到工程tester的目录下。
2: int WINAPI WinMain(HINSTANCE hInstance,
3:
HINSTANCE hPrevInstance,
4:
LPSTR lpCmdLine,
5: int nCmdShow)
6:
{
7:
…
8: return MsgBox();
9:
…
10:
}
在上面的代码中,MsgBox()函数的所有参数都使用了缺省值。
(2) 使用动态链接
如果只有DLL文件,只能采用动态链接的方式了。需要用到三个函数:
LoadLibrary(L"###.dll"); 此处的L,是表示UNICODE,在VS2005中,对字符的处理时默认是按照UNICODE方式处理的。
GetProcAddress(hDllLib,"函数名"); 这个是用于返回所需要调用的DLL中的函数的地址。一般在此前需要声明一个与要调用的函数原型相同的函数指针,用于接收此函数返回的地址指针。
FreeLibrary(hDllLib); 因为DLL是动态加载的,在用完后一定要释放资源。
例如:
1: typedef int (*lpmsgbox)(LPCTSTR , LPCTSTR , UINT );
2:
3:
HINSTANCE hDLL;
4:
lpmsgbox msgbox;
5: hDLL=LoadLibrary(L"msgbox.dll" );//加载动态链接库msgbox.dll文件;
6: msgbox=(lpmsgbox)GetProcAddress(hDLL,"MsgBox" );
7: msgbox(L"Hello" ,L"Hello" ,MB_OK);
8: FreeLibrary(hDLL);//卸载msgbox.dll文件;
在上例中使用类型定义关键字typedef,定义指向和DLL中相同的函数原型指针,然后通过LoadLibray()将DLL加载到当前的应用程序中并返回当前DLL文件的句柄,然后通过GetProcAddress()函数获取导入到应用程序中的函数指针,函数调用完毕后,使用 FreeLibrary()卸载DLL文件。在编译程序之前,首先要将DLL文件拷贝到工程所在的目录或Windows系统目录下。
使用显式链接应用程序编译时不需要使用相应的Lib文件。另外,使用GetProcAddress()函数时,可以利用 MAKEINTRESOURCE()函数直接使用DLL中函数出现的顺序号,如将GetProcAddress(hDLL,"MsgBox")改为 GetProcAddress(hDLL, MAKEINTRESOURCE(1))(函数MsgBox ()在DLL中的顺序号是1),这样调用DLL中的函数速度很快,但是要记住函数的使用序号,否则会发生错误。
最后建议大家进行隐士链接dll。