动态连接库.docx
《动态连接库.docx》由会员分享,可在线阅读,更多相关《动态连接库.docx(19页珍藏版)》请在冰点文库上搜索。
![动态连接库.docx](https://file1.bingdoc.com/fileroot1/2023-4/28/2aaded41-4aba-42a5-a27c-03b55146b96d/2aaded41-4aba-42a5-a27c-03b55146b96d1.gif)
动态连接库
动态链接库(DLL)
一、相关概念
动态链接库(DynamicLinkLibrary):
动态链接库通常都不能直接运行,也不能接收消息。
它们是一些独立的文件,文件后缀一般为.DLL,其中包含供其他可执行程序或其它DLL调用的函数。
只有在其它模块调用动态链接库中的函数时,它才发挥作用。
例如,WindowsAPI中的所有函数都包含在DLL中。
其中有3个最重要的DLL,Kernel32.dll,它包含用于管理内存、进程和线程的各个函数;User32.dll,它包含用于执行用户界面任务(如窗口的创建和消息的传送)的各个函数;GDI32.dll,它包含用于画图和显示文本的各个函数。
他们一般位于C:
\windows\System32或类似的目录下。
通俗一点说,动态链接库就是将很多函数放到一起形成一个集合模块,注册后供其他应用程序运行时动态调用。
这许许多多的函数又可分为内部函数和导出函数。
内部函数是用来在动态链接库内部调用的函数,主要用来实现动态链接库的实际功能;导出函数,顾名思义就是供外部模块或者应用程序在运行的时候调用的,是应用程序和动态链接库间的接口。
导出函数包含在导出表中,导出表包含动态链接库中所有可以被外部调用的函数名(对外的接口)。
顾名思义,动态链接库,是动态链接,它是相对于静态链接而言的。
静态库:
函数和数据被编译进一个二进制文件(通常扩展名为.LIB)。
在其他程序使用静态库的情况下,在编译链接可执行文件时,链接器从库中复制这些函数和数据,并把它们和应用程序的其它模块组合起来创建最终的可执行文件(.EXE文件)。
发布产品时,只需发布.EXE文件即可,不需要发布.LIB文件。
优点:
无须包括函数库所包含的函数代码,应用程序可以利用标准的函数集;
缺点:
两个应用程序运行时同时使用同一静态链接库中的函数,需要使用同一函数代码的两份拷贝,降低内存使用率。
动态库:
在其他程序使用动态库的时候,往往要用到DLL提供者提供的两个文件:
一个引入库(.LIB)和一个DLL(.DLL)。
但这里的.LIB文件和静态库中的.LIB文件有着本质的差别,这里的.LIB只包含导出的函数、变量的符号名,.DLL包含实际的函数和数据。
在编译链接时,只需要链接.LIB,DLL中的函数代码和数据并不复制到可执行文件中,在运行的时候,才去加载DLL,并访问DLL中的导出函数。
优点:
可以采用多种编程语言来编写;提供二次开发的平台;简化项目管理;可以节省磁盘空间和内存,等。
动态链接库DLL和可执行文件EXE的区别:
可执行文件运行起来后有自己的独立的进程空间,而动态链接库的导出函数被动态链接到应用程序的进程空间,这样多个应用程序可以共享一份代码副本。
另外,动态链接库可以包括一个导出表,记录该动态链接库对外提供的函数接口。
二、DLL的开发、声明和调用
1、Win32DLL和MFCDLL的编写。
见下面的例子。
2、DLL中声明导出函数的三种方式:
使用_declspec(dllexport)、使用extern、使用.def文件
3、导出整个类或仅导出类中的部分成员函数。
导出整个类:
在类定义的关键字class后面加_declspec(dllexport)
导出成员函数:
在成员函数定义的最前面加_declspec(dllexport)
4、调用动态链接库的两种方式:
隐式链接:
需在工程设置中添加对.lib的引用或者使用#pragmacomment(lib,"Dll1.lib")
显式加载:
需要调用LoadLibrary()或者类似的函数加载动态链接库,再使用GetProcessAddress()获得要调用的每个函数的函数指针,使用完毕后,调用FreeLibrary()卸载DLL。
这两种加载方式都需要预先声明外部函数的类型,然后才能调用成功。
只是声明的方式有所不同。
三、实例
1:
创建一个Win32DLL工程Dll1
1.1使用_declspec(dllexport)声明导出函数
在Dll1.cpp中输入以下代码:
#include
#include
intAbsSub(inta,intb)//注意,前面没有加_declspec(dllexport)导出声明
{
return(a>b?
a-b:
b-a);
}
_declspec(dllexport)intAdd(inta,intb)//需要导出的函数名前面要加导出声明
{
returna+b;
}
_declspec(dllexport)intSub(inta,intb)
{
returna-b;
}
_declspec(dllexport)intSub(inta,intb,boolbAbs)
{
if(bAbs)
returnAbsSub(a,b);
else
returnSub(a,b);
}
class/*__declspec(dllexport)*/CMyPoint
{
public:
voidPrint(intx,inty)
{
HWNDhWnd=:
:
GetForegroundWindow();
HDChdc=:
:
GetDC(hWnd);
charbuf[20];
memset(buf,0,20);
sprintf(buf,"x=%d,y=%d",x,y);
:
:
TextOut(hdc,10,10,buf,strlen(buf));
:
:
ReleaseDC(hWnd,hdc);
};
voidShow(intx,inty)
{
HWNDhWnd=:
:
GetForegroundWindow();
HDChdc=:
:
GetDC(hWnd);
charbuf[20];
memset(buf,0,20);
sprintf(buf,"x=%d,y=%d",x,y);
:
:
TextOut(hdc,10,30,buf,strlen(buf));//仅仅y坐标不同。
:
:
ReleaseDC(hWnd,hdc);
};
};
classCPerson
{
public:
/*__declspec(dllexport)*/voidShowName(constchar*name)
{
HWNDhWnd=:
:
GetForegroundWindow();
HDChdc=:
:
GetDC(hWnd);
:
:
TextOut(hdc,10,50,name,strlen(name));
:
:
ReleaseDC(hWnd,hdc);
};
};
编译链接后在debug目录下可以找到dll1.lib和dll1.dll两个文件。
注意,如果代码中每个函数名前都没有声明_declspec(dllexport),编译时将不会生产dll1.lib文件。
这样的DLL将没有使用价值。
这里有一个问题,调用这个.DLL的其他程序如何知道其中的函数名称和用法呢?
答案是:
在DOS下使用DLL导出函数查看命令dumpbin/exportsdll1.dll,就可以看到dll1.dll文件包含了上述2个Add和Sub的信息。
Dumpbin.exe文件一般位于C:
\ProgramFiles\MicrosoftVisualStudio\VC98\Bin目录下。
如果在任意目录不能执行Dumpbin,则需要先运行C:
\ProgramFiles\MicrosoftVisualStudio\VC98\Bin\VCVARS32.BAT,使系统将该路径设置为缺省路径。
Dumpbin查看结果如下:
ordinalhintRVAname
1000001005?
Add@@YAHHH@Z
210000100A?
Sub@@YAHHH@Z
3200001014?
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文件导出函数声明解决函数名改编问题(见后面的MFCDLL实例)
.def是由一个或者多个用于描述DLL属性的文本文件。
它包含以下一些模块定义语句:
LIBRARY:
指出动态链接库的名字,链接器负责将该名字放到动态链接库中;
DESCRIPTION:
描述该动态链接库的用途;
EXPORTS:
指出被导出函数的名称和序号;
例如:
LIBRARYDllA
DESCRIPTION“实现数据库的操作”
EXPORTS
Add@1
Delete@2
Member@3
对于使用MFC应用程序向导(AppWizard)生成的动态链接库,应用程序向导(AppWizard)会自动生成一个.def文件;对于非MFCDLL,可手工添加该文件到工程中。
使用dumpbin命令查看,发现函数名字没有被改编。
2、以隐式加载的方式使用DLL
2.1以_declspec(dllimport)方式声明外部函数
创建一个MFC对话框工程DllTest来调用上述DLL中的3个函数。
加入3个按钮,并添加代码。
如下:
_declspec(dllimport)intAdd(inta,intb);//使用_declspec(dllimport)声明DLL函数
_declspec(dllimport)intSub(inta,intb);//注意:
是dllimport,不是dllexport
_declspec(dllimport)intSub(inta,intb,boolbIsAbs);
voidCDllTestDlg:
:
OnBtnAdd()
{
CStringstr;
str.Format("3+4=%d",Add(3,4));
MessageBox(str);
}
voidCDllTestDlg:
:
OnBtnSub()
{
CStringstr;
str.Format("3-4=%d",Sub(3,4));
MessageBox(str);
}
voidCDllTestDlg:
:
OnBtnAbssub()
{
CStringstr;
str.Format("abs(3-4)=%d",Sub(3,4,true));
MessageBox(str);
}
编译发现,出现LNK2001链接错误。
将dll1.lib复制到当前工程目录下,并做如下工程设置:
再次编译将会通过。
但运行却又出现错误,提示找不到dll1.dll文件。
将dll1.dll也复制到当前工程目录下,再次运行后,一切就正常了。
从上面的编译链接和运行的过程可以知道:
dll1.lib文件是链接器进行链接时才需要的,它只是提供了dll1.dll文件中导出函数的函数名等信息,并没有包含实际的函数和数据。
函数的实际功能是在程序运行时动态的调用dll1.dll文件时才加载的。
也就是说,DLL的调用者在开发时需要.lib文件,发行时需要.dll文件。
●也可以不用在工程中进行dll1.lib的设置,在需要调用DLL函数的代码的前面,添加如下语句:
#pragmacomment(lib,"Dll1.lib")
这和上图中在工程设置里写上Dll1.lib的效果一样。
●使用ViaualStudio提供Depends工具可以查看一个EXE文件或者DLL文件在运行时所依赖的所有动态链接库。
如下图所示,说明dlltest.exe文件需要dll1.dll,mfc42d.dll等的支持。
2.2以extern方式声明外部函数
上面是使用_declspec(dllimport)对外部函数进行的声明,它明确告诉编译器,函数是来自于DLL的,也是较好的声明方式。
还有一种extern声明外部函数的方式,将上述dll1.cpp文件的声明中的_declspec(dllimport)替换成extern即可,运行效果也是一样的。
如下:
externintAdd(inta,intb);//使用extern声明DLL中的函数
externintSub(inta,intb);
externintSub(inta,intb,boolbIsAbs);
2.3使用头文件完善函数的声明,明确告知使用者函数的调用方式。
上面讲过,可以使用DOS命令dumpbin来查看DLL的导出函数有哪些,但dumpbin命令看不到函数的具体参数,返回类型等具体的函数原型。
因此,作为DLL的开发者,有必要提供完整的导出函数的原型,明确告知调用者DLL的用法。
为Dll1工程添加Dll1.h头文件,代码如下:
#ifndefDLL1_API
#defineDLL1_API_declspec(dllimport)
#endif
//intAbsSub(inta,intb)//实际工作中删掉这一行而不是注释掉,隐藏设计创意
DLL1_APIintAdd(inta,intb);
DLL1_APIintSub(inta,intb);
DLL1_APIintSub(inta,intb,boolbAbs);
在Dll1.cpp文件的最前面加上对DLL1_API的定义,如下:
//在此对DLL1_API进行声明,表示当前是导出模式。
//而本DLL的最终使用者不需进行任何声明,默认使用导入模式。
#defineDLL1_API_declspec(dllexport)
将Dll1.h文件复制到DllTest工程的目录下,包含进工程的.cpp文件中去,并注释掉原有的_declspec(dllexport)声明。
这样,同一个头文件就可以被开发者和使用者共同使用了。
3、以显式加载的方式使用DLL
这种方式需要调用LoadLibrary()或者类似的函数,来加载动态链接库,再使用GetProcessAddress()获得要调用的每个函数的函数指针,使用完毕后,调用FreeLibrary()卸载DLL。
这种方式不需要*.lib文件,也不需要包含*.h文件。
但需要知道被调用函数的原型,以便为GetProcessAddress()的返回值定义相应的函数指针。
3.1建立一个MFCDLL工程MyDll
3.2在MyDll.cpp文件中
添加函数声明
#include"MyDll.h"
#ifdef_DEBUG
#definenewDEBUG_NEW
#undefTHIS_FILE
staticcharTHIS_FILE[]=__FILE__;
#endif
voidMyFun1(CWnd*h);
voidMyFun2(CWnd*h);
添加函数实现:
CMyDllApptheApp;
voidMyFun1(CWnd*h)
{
CBrushnewBrush;
newBrush.CreateSolidBrush(RGB(0,0,255));
CBrush*pOldBrush;
CClientDCdc(h);
pOldBrush=dc.SelectObject(&newBrush);
dc.Ellipse(50,30,300,200);
dc.SelectObject(pOldBrush);
}
voidMyFun2(CWnd*h)
{
CPennewPen;
CPen*pOldPen;
newPen.CreatePen(PS_SOLID,1,RGB(0,0,255));
CClientDCdc(h);
CRectrect(40,20,310,210);
InvalidateRect((HWND__*)h,&rect,TRUE);
pOldPen=dc.SelectObject(&newPen);
dc.Ellipse(50,30,300,200);
dc.SelectObject(pOldPen);
}
3.3.def文件的编写:
;MyDll.def:
DeclaresthemoduleparametersfortheDLL.
LIBRARY"MyDll"
DESCRIPTION'MyDllWindowsDynamicLinkLibrary'
EXPORTS
;Explicitexportscangohere
MyFun1
MyFun2
注意:
.def文件注释是使用分号;而不是//
3.4编译生成MyDll.Dll文件
将生成的MyDll.dll文件拷贝到D:
\WINDOWS\system32目录下.这一部叫做动态链接库文件的注册.注意:
Debug和Release编译方式生成的该文件位置不一样。
怎么找?
3.5使用动态链接库
1建立使用动态链接库的工程(顺便介绍MFCDLL的使用)
其他步骤默认既可。
涉及如下对话框界面
添加代码:
1.声明全局变量
在TesDllDlg.cpp中添加如下代码:
HINSTANCEhDLL=NULL;//声明全局变量hDLL用于存放DLL的句柄并初始化为空
typedefvoid(*MYFUN1)(CWnd*h);//声明函数指针类型,用它来声明变量MyFun1;
MYFUN1MyFun1;
typedefvoid(*MYFUN2)(CWnd*h);
MYFUN2MyFun2;
如图:
2添加按钮点击消息响应函数
voidCTestDllDlg:
:
OnLoadDll()
{
//TODO:
Addyourcontrolnotificationhandlercodehere
if(hDLL!
=NULL)
{
MessageBox("你已经装载了MyDll.DLL文件!
");
return;
}
hDLL=LoadLibrary("MyDll.DLL");
if(hDLL==NULL)
{
MessageBox("无法装载MyDll.DLL文件!
");
return;
}
MyFun1=(MYFUN1)GetProcAddress(hDLL,"MyFun1");
MyFun2=(MYFUN2)GetProcAddress(hDLL,"MyFun2");
}
voidCTestDllDlg:
:
OnRunDll()
{
//TODO:
Addyourcontrolnotificationhandlercodehere
if(hDLL==NULL)
{
MessageBox("你还没有装载MyDll.DLL文件!
");
return;
}
for(inti=0;i<=10;i++)
{
MyFun1(this);
Sleep(1000);
MyFun2(this);
Sleep(1000);
}
}
程序运行结果:
反复绘制实心的椭圆和空心的椭圆,如下图:
需要特别注意的是,如果函数名字被改编了,在使用GetProcessAddress()时,其第二个参数需要使用改编后的函数名。
另外,使用DEF导出函数时,不允许进行函数重载,但使用_declspec(dllexport)导出时就可以,其原因是_declspec(dllexport)对函数进行了名称改编。
另外也可根据导出函数的序号来获取函数指针。
但不推荐使用这种方法。
如:
MYFUN2Draw2=(MYFUN2)GetProcAddress(hDLL,MAKEINTRESOURCE
(2));
和
MYFUN2Draw2=(MYFUN2)GetProcAddress(hDLL,“MyFun2”);
是一样的效果。
四、DllMain
可有可无。
如有,其中应尽量少的写代码。
可以在此做一些初始化的工作。
BOOLWINAPIDllMain(
HINSTANCEhinstDLL,//handletoDLLmodule
DWORDfdwReason,//reasonforcallingfunction
LPVOIDlpvReserved//reserved
);
作业:
1、提交:
编写一个Win32DLL工程MaxMin,对外提供带2个参数和3个参数的求最大值和最小值的4个导出函数:
intMax(inta,intb,intc);intMax(inta,intb);intMin(inta,intb,intc);intMin(inta,intb);
并另外编写一个MFC对话框工程TestMaxMin,使用隐式链接方式对MaxMin.dll进行测试。
2、思考:
MFCDLL中能导出某个类中的所有函数吗?
如果能,有哪些限制?
为什么?
如何导出?
使用上述Dll1工程中的例子进行测试。
(考点:
DEF和declspec(dllexport)两种导出方式的比较及混合使用)