2024年2月7日发(作者:)
动态链接库(DLL)
一、相关概念
动态链接库(Dynamic Link Library):
动态链接库通常都不能直接运行,也不能接收消息。它们是一些独立的文件,文件后缀一般为.DLL,其中包含供其他可执行程序或其它DLL调用的函数。只有在其它模块调用动态链接库中的函数时,它才发挥作用。
例如,Windows API中的所有函数都包含在DLL中。其中有3个最重要的DLL,,它包含用于管理内存、进程和线程的各个函数;,它包含用于执行用户界面任务(如窗口的创建和消息的传送)的各个函数;,它包含用于画图和显示文本的各个函数。他们一般位于C:windowsSystem32或类似的目录下。
通俗一点说,动态链接库就是将很多函数放到一起形成一个集合模块,注册后供其他应用程序运行时动态调用。
这许许多多的函数又可分为内部函数和导出函数。内部函数是用来在动态链接库内部调用的函数,主要用来实现动态链接库的实际功能;导出函数,顾名思义就是供外部模块或者应用程序在运行的时候调用的,是应用程序和动态链接库间的接口。导出函数包含在导出表中, 导出表包含动态链接库中所有可以被外部调用的函数名(对外的接口)。
顾名思义,动态链接库,是动态链接,它是相对于静态链接而言的。
静态库:
函数和数据被编译进一个二进制文件(通常扩展名为.LIB)。在其他程序使用静态库的情况下,在编译链接可执行文件时,链接器从库中复制这些函数和数据并把它们和应用程序的其它模块组合起来创建最终的可执行文件(.EXE文件)。发布产品时,只需发布.EXE文件即可,不需要发布.LIB文件。
优点:无须包括函数库所包含的函数代码,应用程序可以利用标准的函数集;
缺点:两个应用程序运行时同时使用同一静态链接库中的函数,需要使用同一函数代码的两份拷贝,降低内存使用率。
动态库:
在其他程序使用动态库的时候,往往要用到DLL提供者提供的两个文件:一个引入库(.LIB)和一个DLL(.DLL)。但这里的.LIB文件和静态库中的.LIB文件有着本质的差别,这里的.LIB只包含导出的函数、变量的符号名,.DLL包含实际的函数和数据。在编译链接时,只需要链接.LIB,DLL中的函数代码和数据并不复制到可执行文件中,在运行的时候,才去加载DLL,并访问DLL中的导出函数。
优点:可以采用多种编程语言来编写;提供二次开发的平台;简化项目管理;可以节省磁盘空间和内存,等。
动态链接库DLL和可执行文件的区别:
可执行文件运行起来后有自己的独立的进程空间,而动态链接库的导出函数被动态链接到应用程序的进程空间,这样多个应用程序可以共享一份代码副本。另外,动态链接库可以包括一个导出表,记录该动态链接库对外提供的函数接口。
二、DLL的开发、声明和调用
1、Win32 DLL和MFC DLL的编写。
见下面的例子1和2。
2、DLL中声明导出函数的三种方式:
使用_declspec(dllexport) 、使用extern、使用.def文件
3、导出整个类或仅导出类中的部分成员函数。
导出整个类:在类定义的关键词class后加_declspec(dllexport)
导出成员函数:在成员函数定义前加_declspec(dllexport)
4、调用动态链接库的两种方式:
隐式链接:需在工程设置中添加对.lib的引用或者使用#pragma comment ( lib, " " )
显式加载:需要调用LoadLibrary()或者类似的函数加载动态链接库,再使用GetProcessAddress()获得要调用的每个函数的函数指针,使用完毕后,调用FreeLibrary()卸载DLL。
这两种加载方式都需要预先声明外部函数的类型,然后才能调用成功。只是声明的方式有所不同。
三、实例
1:创建一个Win32 DLL工程Dll1
1.1 使用_declspec(dllexport)声明导出函数
在中输入以下代码:
#include
#include
int AbsSub(int a, int b) //注意,前面没有加 _declspec(dllexport) 导出声明
{
return (a > b ? a - b : b - a);
}
_declspec(dllexport) int Add(int a, int b) //需要导出的函数名前面要加导出声明
{
return a + b;
}
_declspec(dllexport) int Sub(int a, int b)
{
return a - b;
}
_declspec(dllexport) int Sub(int a, int b, bool bAbs)
{
if (bAbs)
return AbsSub(a, b);
}
else
return Sub(a, b);
class /*__declspec(dllexport)*/ CMyPoint
{
public:
void Print(int x, int y)
{
HWND hWnd = ::GetForegroundWindow();
HDC hdc = ::GetDC(hWnd);
char buf[20];
memset(buf, 0, 20);
sprintf(buf, "x = %d, y = %d", x, y);
::TextOut(hdc, 10, 10, buf, strlen(buf));
::ReleaseDC(hWnd, hdc);
};
void Show(int x, int y)
{
HWND hWnd = ::GetForegroundWindow();
HDC hdc = ::GetDC(hWnd);
char buf[20];
memset(buf, 0, 20);
sprintf(buf, "x = %d, y = %d", x, y);
::TextOut(hdc, 10, 30, buf, strlen(buf)); //仅仅y坐标不同。
::ReleaseDC(hWnd, hdc);
};
};
class CPerson
{
public:
/*__declspec(dllexport)*/ void ShowName(const char * name)
{
HWND hWnd = ::GetForegroundWindow();
HDC hdc = ::GetDC(hWnd);
::TextOut(hdc, 10, 50, name, strlen(name));
::ReleaseDC(hWnd, hdc);
};
};
编译链接后在debug目录下可以找到和两个文件。
注意,如果代码中每个函数名前都没有声明_declspec(dllexport),编译时将不会生产文件。这样的DLL将没有使用价值。
这里有一个问题,调用这个.DLL的其他程序如何知道其中的函数名称和用法呢?答案是:在DOS下使用DLL导出函数查看命令dumpbin /exports ,就可以看到文件包含了上述2个Add和Sub的信息。文件一般位于 C:Program
FilesMicrosoft Visual StudioVC98Bin目录下。查看结果如下:
ordinal hint RVA name
1 0 00001005 ?Add@@YAHHH@Z
2 1 0000100A ?Sub@@YAHHH@Z
3 2 00001014 ?Sub@@YAHHH_N@Z
其中RVA列包含的数值表示导出函数在DLL模块中的偏移地址,通过该地址可以在模块中找到该函数。Name列是导出函数的名称,但我们发现函数名字被改编了。
但其中没有AbsSub的信息,这是因为我们没有在AbsSub函数名前加入导出声明_declspec(dllexport)。因此,在要导出的函数名前面一定要加上导出声明。
1.2 使用extern “C” _declspec(dllexport)解决函数名改编问题
加上extern “C”之后,用C++写的DLL就可以在纯C环境使用了,但写DLL时就不能使用类了,因为C语言不支持类。因此,除非特殊需求,一般不使用这种方式。使用dumpbin命令查看,发现函数名字没有被改编。
1.3 使用.def文件导出函数声明解决函数名改编问题(见后面的MFC DLL实例)
.def是由一个或者多个用于描述DLL属性的文本文件。它包含以下一些模块定义语句:
LIBRARY:指出动态链接库的名字,链接器负责将该名字放到动态链接库中;
DESCRIPTION:描述该动态链接库的用途;
EXPORTS:指出被导出函数的名称和序号;
例如:
LIBRARY DataBase
DESCRIPTION “实现数据库的操作”
EXPORTS
Add @1
Delete @2
Member @3
对于使用MFC 应用程序向导(AppWizard)生成的动态链接库,应用程序向导(AppWizard)会自动生成一个.def文件;对于非MFC DLL,可手工添加该文件到工程中。使用dumpbin命令查看,发现函数名字没有被改编。
2、以隐式加载的方式使用DLL
2.1 以_declspec(dllimport)方式声明外部函数
创建一个MFC对话框工程DllTest来调用上述DLL中的3个函数。加入3个按钮,并
添加代码。如下:
_declspec(dllimport) int Add(int a, int b); //使用_declspec(dllimport) 声明DLL函数
_declspec(dllimport) int Sub(int a, int b); //注意:是dllimport,不是dllexport
_declspec(dllimport) int Sub(int a, int b, bool bIsAbs);
void CDllTestDlg::OnBtnAdd()
{
CString str;
("3 + 4 = %d", Add(3, 4));
MessageBox(str);
}
void CDllTestDlg::OnBtnSub()
{
CString str;
("3 - 4 = %d", Sub(3, 4));
MessageBox(str);
}
void CDllTestDlg::OnBtnAbssub()
{
CString str;
("abs(3 - 4) = %d", Sub(3, 4, true));
MessageBox(str);
}
编译发现,出现LNK2001链接错误。将复制到当前工程目录下,并做如下工程设置:
再次编译将会通过。但运行却又出现错误,提示找不到文件。将也复制到当前工程目录下,再次运行后,一切就正常了。
从上面的编译链接和运行的过程可以知道:文件是链接器进行链接时才需要的,它只是提供了文件中导出函数的函数名等信息,并没有包含实际的函数和数据。函数的实际功能是在程序运行时动态的调用文件时才加载的。也就是说,DLL的调用
者在开发时需要.lib文件,发行时需要.dll文件。
也可以不用在工程中进行的设置,在需要调用DLL函数的代码的前面,添加如下语句:
#pragma comment ( lib, " " )
这和上图中在工程设置里写上的效果一样。
使用Viaual Studio提供Depends工具可以查看一个EXE文件或者DLL文件在运行时所依赖的所有动态链接库。如下图所示,说明文件需要,等的支持。
2.2 以extern方式声明外部函数
上面是使用_declspec(dllimport)对外部函数进行的声明,它明确告诉编译器,函数是来自于DLL的,也是较好的声明方式。还有一种extern声明外部函数的方式,将上述文件的声明中的_declspec(dllimport)替换成extern即可,运行效果也是一样的。如下:
extern int Add(int a, int b); //使用extern声明DLL中的函数
extern int Sub(int a, int b);
extern int Sub(int a, int b, bool bIsAbs);
2.3 使用头文件完善函数的声明,明确告知使用者函数的调用方式。
上面讲过,可以使用DOS命令dumpbin来查看DLL的导出函数有哪些,但dumpbin命令看不到函数的具体参数,返回类型等具体的函数原型。因此,作为DLL的开发者,
有必要提供完整的导出函数的原型,明确告知调用者DLL的用法。
为Dll1工程添加Dll1.h头文件,代码如下:
#ifndef DLL1_API
#define DLL1_API _declspec(dllimport)
#endif
//int AbsSub(int a, int b) //实际工作中删掉这一行而不是注释掉,隐藏设计创意
DLL1_API int Add(int a, int b);
DLL1_API int Sub(int a, int b);
DLL1_API int Sub(int a, int b, bool bAbs);
在文件的最前面加上对DLL1_API的定义,如下:
//在此对DLL1_API进行声明,表示当前是导出模式。
//而本DLL的最终使用者不需进行任何声明,默认使用导入模式。
#define DLL1_API _declspec(dllexport)
将Dll1.h文件复制到DllTest工程的目录下,包含进工程的.cpp文件中去,并注释掉原有的_declspec(dllexport)声明。
这样,同一个头文件就可以被开发者和使用者共同使用了。
3、以显式加载的方式使用DLL
这种方式需要调用LoadLibrary()或者类似的函数,来加载动态链接库,再使用GetProcessAddress()获得要调用的每个函数的函数指针,使用完毕后,调用FreeLibrary()卸载DLL。这种方式不需要*.lib文件,也不需要包含*.h文件。但需要知道被调用函数的原型,以便为GetProcessAddress()的返回值定义相应的函数指针。
3.1 建立一个MFC DLL工程MyDll
3.2 在文件中
添加函数声明
#include "MyDll.h"
#ifdef _DEBUG
#define new DEBUG_NEW
#undef THIS_FILE
static char THIS_FILE[] = __FILE__;
#endif
void MyFun1(CWnd *h);
void MyFun2(CWnd *h);
添加函数实现:
CMyDllApp theApp;
void MyFun1(CWnd *h)
{
CBrush newBrush;
SolidBrush(RGB(0,0,255));
CBrush *pOldBrush;
CClientDC dc(h);
pOldBrush = Object(&newBrush);
e(50,30,300,200);
Object(pOldBrush);
}
void MyFun2(CWnd *h)
{
CPen newPen;
CPen *pOldPen;
Pen(PS_SOLID,1,RGB(0,0,255));
CClientDC dc(h);
CRect rect(40,20,310,210);
InvalidateRect((HWND__*)h,&rect,TRUE);
pOldPen = Object(&newPen);
e(50,30,300,200);
Object(pOldPen);
}
3.3 .def文件的编写:
; : Declares the module parameters for the DLL.
LIBRARY "MyDll"
DESCRIPTION 'MyDll Windows Dynamic Link Library'
EXPORTS
; Explicit exports can go here
MyFun1
MyFun2
注意:.def文件注释是使用分号;而不是//
3.4 编译生成文件
将生成的文件拷贝到D:WINDOWSsystem32目录下.这一部叫做动态链接库文件的注册.注意:Debug和Release编译方式生成的该文件位置不一样。怎么找?
3.5 使用动态链接库
1建立使用动态链接库的工程(顺便介绍MFC DLL的使用)
其他步骤默认既可。
涉及如下对话框界面
添加代码:
1.声明全局变量
在中添加如下代码:
HINSTANCE hDLL = NULL; // 声明全局变量hDLL用于存放DLL的句柄并初始化为空
typedef void (* MYFUN1)( CWnd *h);// 声明函数指针类型,用它来声明变量MyFun1;
MYFUN1 MyFun1;
typedef void (* MYFUN2)( CWnd *h);
MYFUN2 MyFun2;
如图:
2 添加按钮点击消息响应函数
void CTestDllDlg::OnLoadDll()
{
// TODO: Add your control notification handler code here
if(hDLL != NULL)
{
MessageBox("你已经装载了文件!");
return;
}
hDLL=LoadLibrary("");
if(hDLL == NULL)
{
MessageBox("无法装载文件!");
return;
}
MyFun1 = (MYFUN1)GetProcAddress(hDLL,"MyFun1");
MyFun2 = (MYFUN2)GetProcAddress(hDLL,"MyFun2");
}
void CTestDllDlg::OnRunDll()
{
// TODO: Add your control notification handler code here
if(hDLL == NULL)
{
MessageBox("你还没有装载文件!");
return;
}
for(int i=0;i<=10;i++)
{
MyFun1(this);
Sleep(1000);
MyFun2(this);
Sleep(1000);
}
}
程序运行结果:反复绘制实心的椭圆和空心的椭圆,如下图:
需要特别注意的是,如果函数名字被改编了,在使用GetProcessAddress()时,其第二个参数需要使用改编后的函数名。
另外,使用DEF导出函数时,不允许进行函数重载,但使用_declspec(dllexport)导出时就可以,其原因是_declspec(dllexport)对函数进行了名称改编。
另外也可根据导出函数的序号来获取函数指针。但不推荐使用这种方法。
如:
MYFUN2 Draw2 = (MYFUN2)GetProcAddress(hDLL, MAKEINTRESOURCE(2));
和
MYFUN2 Draw2 = (MYFUN2)GetProcAddress(hDLL, “MyFun2”);
是一样的效果。
四、DllMain
可有可无。如有,其中应尽量少的写代码。可以在此做一些初始化的工作。
BOOL WINAPI DllMain(
HINSTANCE hinstDLL, // handle to DLL module
DWORD fdwReason, // reason for calling function
LPVOID lpvReserved // reserved
);
作业:
1、提交:
编写一个Win32 DLL工程MaxMin,对外提供带2个参数和3个参数的求最大值和最小值的4个导出函数:int Max(int a, int b, int c); int Max(int a,
int b); int Min(int a, int b, int c); int Min(int a, int b);
并另外编写一个MFC对话框工程TestMaxMin,使用隐式链接方式对进行测试。
2、思考:
MFC DLL中能导出某个类中的所有函数吗?如果能,有哪些限制?为什么?如何导出?使用上述Dll1工程中的例子进行测试。(考点: DEF和declspec(dllexport)两种导出方式的比较及混合使用)


发布评论