最近项目中使用到了DLL,因此就把最近一段时间的学习总结一下,以备不时之需。
一、相关概念
1、动态链接库
自从微软推出第一个版本的Windows操作系统以来,动态链接库(DLL)一直是Windows操作系统的基础。动态链接库通常都不能直接运行,也不能接收消息。它们是一些独立的文件,其中包含能被可执行程序或其它DLL调用来完成某项工作的函数。只有在其它模块调用动态链接库中的函数时,它才发挥作用。WindowsAPI中的所有函数都包含在DLL中。其中有3个最重要的DLL,Kernel32.dll,它包含用于管理内存、进程和线程的各个函数;User32.dll,它包含用于执行用户界面任务(如窗口的创建和消息的传送)的各个函数;GDI32.dll,它包含用于画图和显示文本的各个函数。
2、静态库和动态库
静态库:函数和数据被编译进一个二进制文件(通常扩展名为.LIB)。在使用静态库的情况下,在编译链接可执行文件时,链接器从库中复制这些函数和数据并把它们和应用程序的其它模块组合起来创建最终的可执行文件(.EXE文件)。在使用动态库的时候,往往提供两个文件:一个引入库和一个DLL。引入库包含被DLL导出的函数和变量的符号名,DLL包含实际的函数和数据。在编译链接可执行文件时,只需要链接引入库,DLL中的函数代码和数据并不复制到可执行文件中,在运行的时候,再去加载DLL,访问DLL中导出的函数。
3、使用动态链接库的好处
可以采用多种编程语言来编写。
增强产品的功能。
提供二次开发的平台。
简化项目管理。
可以节省磁盘空间和内存。
有助于资源的共享。
有助于实现应用程序的本地化。
4、动态链接库被多个进程访问
5、动态链接库加载的两种方式
隐式链接
显示加载
二、动态链接库的使用
1、头文件内容
在头文件的最前面加上一下这样一段代码
/** 方法一 */
#ifdef DLL_EXPORT
#define DLL_API __declspec(dllexport)
#else
#define DLL_API __declspec(dllimport)
#endif // DLL_EXPORT /** 方法二 */
#ifdef DLL_EXPORT
#define DLL_API __declspec(dllexport)
#else
#define DLL_API
#endif // DLL_EXPORT
方法一的意思是根据是否定义DLL_EXPORT而将DLL_API分别定义为__declspec(dllexport)或是__declspec(dllimport) ,用来表示DLL_API修饰的部分是导入还是导出。因为DLL的头文件在编译DLL的时候需要
声明导出方式,当用户使用已发布的DLL链接库的时候,DLL的头文件应该作为导入方式使用,因此在编译DLL库的时候,需要在#include “dll.h”的前面加入一句#define DLL_EXPORT,而动态库的使用方则
不需要任何改变。
方法二将DLL_API定义为空,也可以,但是当导出类的静态对象的时候,后出现问题,因此推荐第一种方法。
2、声明导出函数
函数导出有两种方式,第一种使用__declspec(dllexport)声明导出函数,第二种使用使用.def文件声明。
- 使用 .DEF 文件的优缺点
- 在 .DEF 文件中导出函数使您得以控制导出序号。当将附加的导出函数添加到 DLL 时,可以给它们分配更高的序号值(高于任何其他导出函数)。当您进行此操作时,使用隐式链接的应用程序
不必与包含新函数的新导入库重新链接。这非常重要,例如,在设计将由许多应用程序使用的第三方 DLL 时。可以通过添加附加的功能不断地增强 DLL,同时确保现有应用程序继续正常使用新的
DLL。MFC DLL 是用 .DEF 文件生成的。
- 使用 .DEF 文件的另一个优点是:可以使用 NONAME 属性导出函数,该属性仅将序号放到 DLL 的导出表中。对具有大量导出函数的 DLL,使用 NONAME 属性可以减小 DLL 文件的大小。有关
编写模块定义语句的信息,请参见模块定义语句的规则。有关序号导出的更多信息,请参见按序号而不是按名称从 DLL 导出函数。
- 使用 .DEF 文件的主要缺点是:在 C++ 文件中导出函数时,需要将修饰名放到 .DEF 文件中,或者通过使用外部“C”用标准 C 链接定义导出函数,以避免编译器进行名称修饰。
- 如果需要将修饰名放到 .DEF 文件中,可以通过使用 Dumpbin 工具或通过使用 /MAP 链接器选项来获取修饰名。请注意,编译器产生的修饰名是编译器特定的。如果将 Visual C++ 编译器产生的修饰名放到 .DEF 文件中,则链接到 DLL 的应用程序必须也是用相同版本的 Visual C++ 生成的,这样调用应用程序中的修饰名才能与 DLL 的 .DEF 文件中的导出名相匹配。
2. 使用 __declspec(dllexport) 的优缺点
使用 __declspec(dllexport) 非常方便,因为不需要考虑维护 .DEF 文件和获取导出函数的修饰名。但是,无法控制编译器生成的导出序号。此方法适合某些情况,例如,在设计要与控制的应用程序
一起使用的 DLL 时;如果用新导出重新生成 DLL,则还需要重新生成应用程序。
首先介绍第一种方法。
/** 导出全局函数 */
DLL_API int global_fun(); /** 导出整个类 */
class DLL_API dllBase
{
public:
dllBase ( void );
~dllBase ( void );
int get();
private:
static int base;
list<string> m_list;
}; /** 导出类的某个成员和方法 */
class dllBase
{
public:
dllBase ( void );
~dllBase ( void );
DLL_API int get();
private:
DLL_API int base;
list<string> m_list;
};
利用VS工具目录下的命令提示符进入相应DLL所在目录,然后用dumpbin命令查看导出函数,或者使用view Dependencies工具直接打开dll文件我们可以看到如图所示界面,界面介绍如图所示
命令:..\Debug> dumpbin -exports dll.dll
注意到先前的函数名get变为?get@dllBase@@QAEHXZ,因此这样的函数只能在由相同编译器编译的程序中进行调用,而其他程序调用则会出错。之所以篡改名字是为了C++的函数重载!
若要希望生成的函数名不变,则需要将之前#define DLL_API _declspec(dllexport)改变为:
#define DLL_API extern "C" _declspec(dllexport)
该方法的缺陷:只能对全局函数,而不能正确地导出类的成员函数。如果我们导出的函数的调用约定改变,即使我们用了extern "C"做声明,也会发生改编。
第二种方式导出函数:
在工程中新建.def文件,在其中写如下信息(其中get为要导出的函数的名称)
LIBRARY DLL //DLL表示dll文件名
EXPORTS //表示要导出那些函数
get
注意:由此生成的文件不会改变导出函数名
从应用程序调用动态链接库的方法
3调用方法
1:隐式链接
1、将DLL文件夹DEBUG下生成的DLL文件,.lib文件拷贝到测试的应用程序(DLL_test)的工程目录下
2、在应用程序的开发界面中,项目->DLL_test的属性,在其属性页中,链接器->输入->附加的依赖项中添加刚才生成的DLL.lib导入文件。
3、在调用函数前,使用extern关键字声明调用的函数是来自外部的程序或者使用_declspec(dllimport)关键字。如本程序中:
//方法1:告诉编译器我们所引用的符号是外部程序 //效率相对方法2低
extern int add(int a,int b);
extern int sub(int a,int b);
//方法2:告诉编译器我们所引用的符号是从.lib导入文件中导入的动态链接库程序 //效率相对高
_declspec(dllimport) int add(int a,int b);
_declspec(dllimport) int sub(int a,int b);
//方法3:在设计DLL的时候就把方法2中的代码添加到头文件,然后在当前文件的#include中包含#include "..\DLL.h" //好处就是如果DLL文件和应用程序不是同一人编写的时候可以更好地调用!
#include "..\DLL.h"
//方法4:宏定义,这样可以用一个宏定义来完成_declspec(dllimport) _declspec(dllemport),而且可以使该宏不仅可以给动态链接库的开发方使用,也可以给动态链接库的调用方来使用
//在DLL.h(DLL设计文件)
#ifdef DLL_API
#else
#define DLL_API _declspec(dllimport)
#endif
我们采用第四种办法。
调用方法2,:动态调用
//动态调用动态链接库
HINSTANCE hInst;
hInst = LoadLibrary ( "Dll.dll" );
typedef int ( *ADDPROC ) ( int a, int b ); //如果在DLL设计过程中是用_stdcall的,那这里也要用_stdcall,否则系统将报错!
//typedef int (_stdcall *ADDPROC)(int a,int b); //"get"这里为用dumpbin –exports dll.dll中显示的名字,如果为?get@dllBase@@QAEHXZ,
//那么在这里也应该写ADDPROC Add=(ADDPROC)GetProcAddress(hInst," ?get@dllBase@@QAEHXZ ");
ADDPROC Get = ( ADDPROC ) GetProcAddress ( hInst, "get" ); //利用序号来调用
//ADDPROC Get=(ADDPROC)GetProcAddress(hInst,MAKEINTRESOURCE(1));
if ( !Get )
{
MessageBox ( "获取函数地址失败!" );
return;
}
int res = Get(); FreeLibrary ( hInst );