Windows程序设计__孙鑫C++Lesson19《动态链接库》

来源:互联网 发布:net编程教训 编辑:程序博客网 时间:2024/06/05 17:36

Windows程序设计__孙鑫C++Lesson19《动态链接库》

本节要点:
1.动态链接库与静态链接库
2.查看动态链接库中导出函数和程序中导入函数
3.动态链接库加载的的隐式连接
4.动态链接库函数的外部提供
5.动态链接库导出函数的名字改编
6.整个类的导出及类的部分函数的导出
7.动态链接库的显式加载和卸载
8.利用MFC AppWizard(dll)新建Dll工程时选项的说明
//***********************************************************************************************
1.动态链接库与静态链接库
关于动态链接库和静态链接库介绍如下图所示:

2.查看动态链接库中导出函数(前提是函数已经导出了)
利用VC 提供的程序dumpbin.exe来查看或者利用其工具Depends来查看。
利用dumpbin.exe:
运行cmd.exe,首先建立环境信息,在cmd.exe中运行 "C:\Program Files\Microsoft Visual Studio\VC98\Bin\VCVARS32.BAT"
然后使用dumpbin的命令来查看.
查看导出情况,命令如,dumpbin -exports Dll1.dll
查看导入情况,命令如,dumpbin -imports DllTest.exe >1.txt(这里表示将结果显示到1.txt文件中)
查看Dll的导出或者exe的导入信息过程如下图所示:

 

3.动态链接库加载的的隐式连接
隐式连接时注意包含动态链接库的Lib文件,Lib文件包含了函数的说明。
隐式连接动态链接库需要声明函数原型,方法有三种,如下:
第一种:extern int add(int a,int b);
         extern int sustract(int a,int b);
第二种: _declspec (dllimport)int add(int a,int b);
       _declspec (dllimport)int sustract(int a,int b);//外部函数 连接时要引入库文件 这种方式效率更高 动态链接库时都应该使用这种方式
第三种:包含头文件法 使编程更加工程化
连接时错误:
Linking...
DllTestDlg.obj : error LNK2001: unresolved external symbol "int __cdecl add(int,int)" (
?add@@YAHHH@Z)
DllTestDlg.obj : error LNK2001: unresolved external symbol "int __cdecl sustract(int,int)" (
?sustract@@YAHHH@Z)
解决将Dll1.lib文件拷贝到DllTest目录下.Dll1.lib包含了Dll1.dll包含了动态链接库中包含的函数名和变量名。
连接lib文件并且将Dll1.dll拷贝到当前目录下(否则出现运行时错误)。
4.动态链接库函数的外部提供
怎么让其他开发人员了解我们动态链接库的导出函数,可以通过提供头文件形式给别的开发人员。
这里有一个技巧,使用宏定义可以让头文件中的函数声明既为本程序服务,又为其他开发人员服务。使用宏定义导出函数形式如下:
//Dll1.h
#ifdef DLL1_API
#else
#define DLL1_API  _declspec (dllimport) 
#endif
DLL1_API int add(int a,int b);
DLL1_API int sustract(int a,int b);
//Dll1.cpp
#define DLL1_API  _declspec (dllexport)
#include "Dll1.h"
int  add(int a,int b)
{
 return a+b;
}
int sustract(int a,int b)
{
 return a-b;
}
这里应该体会到宏定义的展开过程。在本程序中因为定义了DLL1_API  所以包含头文件时默认不做什么,
那么DLL1_API 就被展开为_declspec (dllexport)表明这是导出函数;
当其他程序包含头文件"Dll1.h" 时,只要不定义DLL1_API ,则宏展开为_declspec (dllimport),表示导入函数。利用宏定义减轻了函数声明的负担。
5.动态链接库导出函数的名字改编
不同的C++编译器可能采用不同规则进行名字改编,也就是你在编写Dll文件中的函数名称对外部来说发生了改变,名字改编可能引发程序连接时错误。
例如动态链接库编写如下:
//Dll3.cpp
_declspec (dllexport) int add(int a ,int b)//导出函数时编译器会做名字改编  add改编为
?add@@YAHHH@Z
{
 return a+b;

}
//测试调用代码如下:
//显式加载和卸载动态链接库
void CDllTestDlg::OnBtnAdd()
{
 // TODO: Add your control notification handler code here
 //动态加载链接库
 HINSTANCE hinst;
 hinst=LoadLibrary("Dll3.dll");//路径名正确填写
 //获取动态链接库中函数地址
 typedef int (*AddProc)(int a,int b);//函数指针类型 在需要时定义指针变量接受函数地址
    //AddProc pfnAdd=(AddProc)GetProcAddress(hinst,"add");//Dll3发生了名字改编 调用无效 

    //使用改编的名字?add@@YAHHH@Z成功调用
  //AddProc pfnAdd=(AddProc)GetProcAddress(hinst,"
?add@@YAHHH@Z");

   AddProc pfnAdd=(AddProc)GetProcAddress(hinst,MAKEINTRESOURCE(1));//使用函数编号成功
 //注意函数指针类型转换
    if(!pfnAdd)
 {
     MessageBox("获取函数地址失败!");
  return;
 }
 CString msg;
 msg.Format("5+3=%d",pfnAdd(5,3));
 MessageBox(msg);
 FreeLibrary(hinst);//卸载动态链接库
}
导出函数名字时使用extern "C"可以解决上述问题。使用extern "C"解决了C++和c语言之间问题,但是Extern C 不能用来到处一个类的成员函数 只能到处全局函数。
//Dll1.h
#ifdef DLL1_API
#else
#define DLL1_API extern "C"   _declspec (dllimport) 
#endif
DLL1_API int  add(int a,int b);
DLL1_API int  sustract(int a,int b);
//Dll1.dll
#define DLL1_API  extern "C"  _declspec (dllexport)
#include "Dll1.h"
int  add(int a,int b)
{
 return a+b;
}
int  sustract(int a,int b)
{
 return a-b;
}
下图表示未使用extern "C"时发生的名字改编:


下图表示使用extern "C"时未发生名字改编:

:

但是使用Extern C仍然存在问题,当函数声明为_stdcall,形式时仍然会发生名字改编改编后如下图所示:

为了解决这个问题可以使用,模块定义文件def文件。调用约定改变为标准调用时不会发生名字改编。
例如程序中使用模块定义文件如下:
//***********************************************************************************************
//Dll2.def

LIBRARY Dll2

EXPORTS
add //函数名
sustract
//***********************************************************************************************
//Dll2.cpp
int _stdcall add(int x,int y)
{
 return x+y;

}
int _stdcall sustract(int x,int y)
{
 return x-y;

}
使用模块定义文件即使使用了_stdcall,动态链接库的导出函数名仍然不会改变。
但是访问函数时需要指明调用约定,否则会出错。以下代码示例了声明为标准调用时必须在调用时也使用标准调用。
//*************************************************************************************************
//显式加载和卸载动态链接库
void CDllTestDlg::OnBtnAdd()
{
 // TODO: Add your control notification handler code here
 //动态加载链接库
 HINSTANCE hinst;
 hinst=LoadLibrary("Dll2.dll");//路径名正确填写
 //获取动态链接库中函数地址
 //typedef int (*AddProc)(int a,int b);//函数指针类型在动态链接库中函数声明为标准调用时,采用这种形式错误
 typedef int (_stdcall *AddProc)(int a,int b);// 标准调用的函数指针类型 
         AddProc pfnAdd=(AddProc)GetProcAddress(hinst,"add");//定义了一个函数指针变量 利用这个指针调用函数
 //注意函数指针类型转换
    if(!pfnAdd)
 {
     MessageBox("获取函数地址失败!");
  return;
 }
 CString msg;
 msg.Format("5+3=%d",pfnAdd(5,3));
 MessageBox(msg);
 FreeLibrary(hinst);//卸载动态链接库
}
//***********************************************************************************************
如果不定义为标准调用的函数指针类型,则发生错误,错误的提示如下图所示:

总之注意一点:定义为标准调用约定使用时也要标准调用函数。
关于调用约定有好几种,这点留待以后学习,暂时未作讨论。
6.整个类的导出及类的部分函数的导出
(1)导出整个类
//***********************************************************************************************
//导出整个类
#ifdef DLL1_API
#else
#define DLL1_API  _declspec (dllimport) 
#endif
class  DLL1_API Point
{
public:
 void  Output(int x,int y);
 void  Test();
};
void Point::Output(int x,int y)
{
    HWND hWnd=GetForegroundWindow();//获得调用者进程的当前正在使用的句柄
    HDC hDC=GetDC(hWnd);//获取DC句柄
    char buf[20];
    memset(buf,0,sizeof(buf));
    sprintf(buf,"x=%d,y=%d",x,y);
    TextOut(hDC,0,0,buf,strlen(buf));//输出信息
 ReleaseDC(hWnd,hDC);
}
//Test函数仅为导出查看 不完成实际功能
void Point::Test()
{
   
}

导出整个类函数如下图所示:


//***********************************************************************************************
(2)导出类的部分函数
//***********************************************************************************************
#ifdef DLL1_API
#else
#define DLL1_API  _declspec (dllimport) 
#endif
//导出部分函数
class  Point
{
public:
 void  DLL1_API Output(int x,int y);//导出类中的一个成员函数
 void  Test();//不会被导出
};

导出部分函数如下图所示:


//***********************************************************************************************
导出类的使用代码如下:
void CDllTestDlg::OnBtnPt()
{
 // TODO: Add your control notification handler code here
 Point pt;
 pt.Output(3,5);
}
运行效果如下图所示:

 

 //***********************************************************************************************

7.动态链接库的显式加载和卸载
(1)
加载动态链接库使函数LoadLibrary,其函数原型为:
FARPROC GetProcAddress(
  HMODULE hModule,    // handle to DLL module
  LPCSTR lpProcName   // function name
);
卸载动态链接库使用函数FreeLibrary。
FreeLibrary 减少动态链接库的引用计数 减为零时模块将被调用进程的地址控件卸载。
隐式加载的动态链接库,整个程序中都可以使用,但是动态链接库通过隐式连接方式加载到内存,会加大进程启动时间,而且肯能很多动态链接库中的函数没有引用;
而动态加载动态链接库可以避免这一点。动态加载的不会在dumpbin中出现。
(2)动态加载的链接库中函数获取使用GetProcAddress函数。注意函数函数指针转换.否则引发如下错误:
error C2440: 'initializing' : cannot convert from 'int (__stdcall *)(void)' to 'int (__cdecl *)(int,int)'
This conversion requires a reinterpret_cast, a C-style cast or function-style cast
显式加载和卸载动态链接库示例代码如下:
//***********************************************************************************************
//显式加载和卸载动态链接库
void CDllTestDlg::OnBtnAdd()
{
 // TODO: Add your control notification handler code here
 //动态加载链接库
 HINSTANCE hinst;
 hinst=LoadLibrary("Dll2.dll");//路径名正确填写
 //获取动态链接库中函数地址
 typedef int (*AddProc)(int a,int b);//函数指针类型 在需要时定义指针变量接受函数地址
       AddProc pfnAdd=(AddProc)GetProcAddress(hinst,"add");//定义了一个函数指针变量 利用这个指针调用函数
 //注意函数指针类型转换
      if(!pfnAdd)
 {
     MessageBox("获取函数地址失败!");
  return;
 }
 CString msg;
 msg.Format("5+3=%d",pfnAdd(5,3));
 MessageBox(msg);
 FreeLibrary(hinst);//卸载动态链接库
}
//***********************************************************************************************
8.利用MFC AppWizard(dll)新建Dll工程时选项的说明
常规链接库    MFC 静态链接库     只需要发布你的动态链接库文件其中包含了MFC链接库
常规链接库    MFC 共享的链接库   发布动态链接库时要确保用户机器上安装有MFC的链接库 否则你的链接库就就不能被正确加载
扩展的链接库  MFC 共享的链接库   与常规的区别在于导出MFC的类时就要创建扩展库。
本节小结:
掌握动态链接库的两种调用方式,动态链接函数的导出包括类的导出,尤其要注意动态链接库中函数的调用约定,这中错误应该尽量避免。