关闭

动态库编程详解

标签: DLL动态库
418人阅读 评论(0) 收藏 举报
分类:

转自:http://blog.csdn.net/mingojiang

目录

概述

一、动态库概念与分类

1、什么是动态库

2、动态库分类

4、动态库解决的问题

二、动态库的创建

1、规则动态库

2、声明导出函数的两种方式

2.1__declspec(dllexport)导出

2.2 .def文件导出

3、导出导入类

三、隐式、显示调用动态库

1、动态库隐式调用

2、动态库显示调用

3.显示、隐式调用的区别

四、动态库的测试

 

 

 

概述

      动态库是继静态库发展起来的一种封装重用技术,在灵活性、扩展性、重用性各方面取得了突破,本文只介绍动态库的基础知识,关于动态库的高级编程,推荐参看Jeffrey Reichter与Chiristophe Nasarre合著的《Windows核心编程》第5版。

      

一、动态库概念与分类

1、什么是动态库

   DLL(Dynamic Linkable Library)动态链接库亦简称动态库,它是一块封装好的代码块,包含着一些方法,一般不包括消息循环,也建议不要去包含这些。可把它看成一个仓库,其提供了可直接使用的变量、函数、类等。打个不太生动的比喻,动态库犹如保卫森严的生产基地,但你可以通过正确入口进入,获得你想要的东西,你不用管也管不着这东西是怎么生产的,拿走从出口出来就行,同时生产基地是共享的,大家都可以通过入口获得相应的东西。

      在“库”的发展史上经历了“无库---静态库---动态库”的时代,无论是动态库还是静态库都能解决代码共享的问题。

动态库是基于二进制级重用的,所以与语言无关、环境无关(前提是你动态库中没有涉及对环境有依赖的东西,如调用一些第三方DLL等)的,再一个得遵循DLL接口规范和调用约定,简而言之,用各种语言编写的标准DLL其他语言都可以调用。所以如果想创建一个通用的DLL,那么得严格遵守DLL规范,包括导出、调用约定、形参几方面的内容。

 

2、动态库分类 

通过VC++工具编写的动态库分为两类----规则DLL与非规则动态库,Visual C++支持编写三种DLL,它们分别是Non-MFC DLL(非MFC动态库)、MFC Regular DLL(MFC规则DLL)、MFC Extension DLL(MFC扩展DLL)。非MFC动态库不采用MFC类库结构,其导出函数为标准的C接口,能被非MFC或MFC编写的应用程序所调用;MFC规则DLL包含一个继承自CWinApp的类,但其无消息循环;MFC扩展DLL采用MFC的动态链接版本创建,它只能被用MFC类库所编写的应用程序所调用。

4、动态库解决的问题

      节省资源:如果采用静态链接库,则无论你愿不愿意,lib中的指令都被直接包含在最终生成的EXE文件中了。但是若使用DLL,该DLL不必被包含在最终EXE文件中,EXE文件执行时可以“动态”地引用和卸载这个与EXE独立的DLL文件。

      节省内存,假如本地有多个进程用到动态库,那么在内存只是只载入一次的,两个进程共用该DLL在内存中的页面。

灵活性:我认为灵活性才是动态库最值得称道的地方,发行的动态库,只要原有的接口不改变,那么你可以任意地改动,增加动态库里面的内容,同时以新版本替换旧版本,而不影响依赖于此动态库的程序。举个例子吧,例如你的一个软件已经发行了,如果出了什么问题,只要找出出问题的模块(假如它就是个动态库),并处理好,把新版的动态库给用户替换旧版的就OK了,如果全是静态库呢?那只有把整个工程编译一编,发一个大包给人家,人家也许还要把以前的软件卸载,重新装一次新的版本。

模块化:这对大项目特别有利,利用动态库可以把项目切割成N个小块加以分工,还有利于错误定位等,岂不快哉。

语言无关性:只要遵循约定的DLL接口规范和调用方式,用各种语言编写的DLL其他语言都可以相互调用。

特殊用途:windows提供的一些特性只有通过DLL才用使用,如钩子(hook)就需要通过DLL来实现,关于钩子的编程细节,将在后续的博文中详细介绍。

 

二、动态库的创建

1、规则动态库

   以VC++6.0为开发工具,在VC++中创建Win32 Dynamic-Link Library工程,下图中选择第一项,当然也可以选择其他项。

在创建的工程中创建一个头文件MyDll.h与一个CPP文件MyDll.cpp,在头文件与源文件中写入以下代码:

//MyDll.h

#ifndef MYDLLEIPORT

#define MYDLLEIPORT extern "C" __declspec(dllimport)

#endif

MYDLLEIPORT int add(int a, int b);

MYDLLEIPORT int     g_nCount;

 

定义实体:

#define MYDLLEIPORT extern "C" __declspec(dllexport)

#include "MyDll.h"

int g_nCount = 0;

int add(int a, int b){

      return (g_nCount += a + b);

}

代码写完了,现在的工作是怎么把函数导出的问题了,下面分析如何导出函数,与导出的方式。

2、声明导出函数的两种方式

DLL中导出函数的声明有两种方式:一种是在函数声明中加上__declspec(dllexport),调用DLL时配合.lib文件通过__declsped(dllimport)指令导入函数即可,另外一种方式是采用模块定义(.def)文件声明,.def文件为链接器提供了有关被链接程序的导出、属性及其他方面的信息。

通过修饰符__declspec(dllexport)导出的函数,可能会面临函数名改编的问题,而通过.def文件导出的,将不会有此问题,要导出编译时函数名不会被改编的函数,还有一种方法,就是在DLL源文件中输入以下代码:

#pragma comment(linker, “/export:add=_add@8”)

不过此方法有个麻烦事,你得事先知道函数改篇后的名字是什么,如函数add改编后是_add@8,还有此方法导出的函数名是两个,”add”与”_add@8”,个人建议使用.def文件导出最为方便快捷,省去非常多的麻烦。

      这两种导出方式都会生成一个与Dll同名的.lib文件,此文件在隐式调用时得用。.lib文件只是包含一些导出函数的属性说明信息。

2.1__declspec(dllexport)导出

   在头文件声明函数前加上__declspec(dllexport),如下:

#ifndef MYDLLEIPORT

#define MYDLLEIPORT extern "C" __declspec(dllimport)

#endif

MYDLLEIPORT int add(int a, int b);//导出函数

MYDLLEIPORT int g_nCount;//导出变量

其中extern"C"表示函数是安C语言的方式编译,C++对函数名称编译时会改编如add,C++编译后变成_add@8,改名后,C语言或者其他语言调用的时候会出现问题,会找不到,同时不同厂家的编译器编译时生成的函数名也可能不一样; __declspec(dllexport)是导出修饰符,指明导出此函数,不用过分解释,照用就行;下面是对extern "C"的解释:

extern "C"包含双重含义,从字面上即可得到:首先,被它修饰的目标是“extern”,表示可被外部文件引擎;其次,被它修饰的目标是“C”的,表示按照C语言方式编译和连接。

编译规范(extern "C")必须指定,才能编译出规范的DLL接口。用这个方法导出接口,如果用VC++编译,还是会有函数名改编的问题,编译后,函数名应该是_add@8,调用DLL时,如果用同一厂家(VC++)编译器,是不是会有问题的,用其他厂家的编译器,那就不能保证了,所以这种导出方法并不是很好,可采用下面介绍的通过.def文件导出的方法。

2.2 .def文件导出

      此方法导出函数,将避免函数名改编的问题。在工程中添加一.def文件---MyDll.def:

在新加入的MyDll.def文件中输入以下代码:

;MyDll.def :导出DLL函数

LIBRARY DLLDemo

EXPORTS

add @ 1

g_nCount DATA

.def文件的规则为:
  (1)LIBRARY语句说明.def文件相应的DLL;
  (2)EXPORTS语句后列出要导出函数的名称或者变量。可以在.def文件中的导出函数

名后加@n,表示要导出函数的序号为n,在进行函数调用时,这个序号将发挥其作用,

可通过序号来调用相应的函数(通过序号取得函数方法:GetProcAddress(hDll,

MAKEINTRESOURCE(1))),不过此方法不推荐,因为难以维护之类的原因,现在基本

上不怎么使用;
  (3).def文件中的注释由每个注释行开始处的分号(;)指定,且注释不能与语句共享一行。

      (4)导出全局变量格式:全局变量DATA,DATA是关键字,例:g_nCount DATA

其实也可简单地写成如下格式:

EXPORTS

add

g_nCount DATA

不需要序号,只给个函数名就OK,记住别把函数名弄错了。

 

3、导出导入类

   最好不要导出类,类的导出会破坏DLL的通用性,C++写的类,C#或者其他语言不一定支持,在这里只是简单的说一下。

//文件名:point.h,point类的声明
#ifndef   POINT_H
#define  POINT_H
#ifdef  DLL_FILE
 class _declspec(dllexport) point //导出类point
#else
 class _declspec(dllimport) point //导入类point
#endif
{
 public:
  float y;
  float x;
  point();
  point(float x_coordinate, float y_coordinate);
};

#endif

此处有宏POINT_H、DLL_FILE,第一个宏的作用当然是防止头文件重编译,第二个宏则是用来判断当前是用来导出还是导入的,在此头的实现文件(.cpp)中先对DLL_FILE定义,则导出此文件,而当用户用这个动态库时,因为不知道DLL_FILE,所以自然没定义,则会导入此文件。
//文件名:point.cpp,point类的实现
#ifndef DLL_FILE
 #define DLL_FILE
#endif
#include "point.h"
//类point的缺省构造函数
point::point()
{
 x = 0.0;
 y = 0.0;
}

类的导出导入看似乎不适合动态调用,因为要包含其头文件,在主调应用程序中生成对象。注意类的导出导入格式如下:

class _declspec(dllexport) 类名 //导出类point
    class _declspec(dllimport) 类名 //导入类point

三、隐式、显示调用动态库

1、动态库隐式调用

   隐式调用也有通过指令与通过IDE设置两种方式。隐式调用需要.lib文件,把.dll与.lib放同一目录下,然后按以下方法操作

Ø 用#pragma comment指令:

      #include “..\MyDll.h”----可以是绝对路径,也以的相对路径

      #pragma comment(lib, “..\\DllDemo.lib”) ----可以是绝对路径,也可以是相对路径

这种方法似乎有点麻烦,必须把路径设对了,不然可能就找不到。

Ø 在编译器(VC)中设置:

1、依次选择tools->options->directories->Show directories for->Library files然后

添加DllDemo.lib的路径。

2、依次选择tools->options->directories->Show directories for->Include files然后选择

MyDll.h的头文件的路径。

      3、依次选择Proctect->Setting->Link然后在Object/library modules中添加上自定义

.lib库(DllDemo.lib)。

      4、使用自定义库中的函数的时候#include “MyLib.h”就可以了。

示例代码:

#include "..\\MyDll.h"

#pragma comment(lib, "..\\Debug\\DLLDemo.lib")

int nResutl = add(1, 2);//直接调用

2、动态库显示调用

   显示调用,不需要头文件,也不需要.lib库文件,随时载入随时释放,比较灵活。

查看动态库信息的工具:

VC++的安装目录下的Depends工具,可以看到动态库的接口信息。

显式调用:

显示调用首先得知道Dll的路径,然后通过API(LoadLibrary)装载DLL,获得DLL的句柄,通过API(GetProcAddress)配合LoadLibrary返回的句柄再找到想要调用的函数的地址(返回的是指向对应函数地址的指针),通过函数指针就可以调用函数了。就是这么简单,以下是方法:

//定义函数指针类型,用以调用动态库中函数,以下是定义函数指针类型的格式:

//typedef [返回类型] (__stdcall *[指针名])(形参);

typedef int (* PADD)(int, int); //定义指针类型
int main(int argc, char *argv[])
{  HINSTANCE hDll; //DLL
句柄
 PADD addFun; //函数指针

HINSTANCE hInstance = LoadLibrary(".\\DllDemo.dll");
if (hInstance != NULL) {    

      addFun = (PADD)GetProcAddress(hInstance, "add");//获得函数地址

      int nResult = pAdd(1, 6);//通过函数指针调用函数

      int *pCount = (int *)GetProcAddress(hInstance, "g_nCount");//获得全局变量指针

      int nCount = *pCount;

FreeLibrary(hInstance );                //记得释放哦
}
return 0;
}

显示调用与隐式调用却有较大差异,下面我们来逐一分析。
  首先,语句typedef int ( * lpAddFun)(int,int)定义了一个与add函数接受参数类型和返回值均相同的函数指针类型。随后,在main函数中定义了lpAddFun的实例addFun;

  其次,在函数main中定义了一个DLL HINSTANCE句柄实例hInstance,通过Win32 Api函数LoadLibrary动态加载了DLL模块并将DLL模块句柄赋给了hDll;

  再次,在函数main中通过Win32 Api函数GetProcAddress得到了所加载DLL模块中函数add的地址并赋给了addFun。经由函数指针addFun进行了对DLL中add函数的调用;

  最后,应用工程使用完DLL后,在函数main中通过Win32 Api函数FreeLibrary释放了已经加载的DLL模块。

      :如果是用.def文件定义的导出函数,并为函数给定的序号,则可用以下方法调用函数被调用的方法,GetProcAddress(hDll, MAKEINTRESOURCE(1))

 

3.显示、隐式调用的区别

   从使用层面上讲,隐式调用方便,载入一次(主进程启动的时候载入),就可以到处使用,而显示调用则麻烦些,得载入释放,还有取得相应函数的地址之类。然而显示调用灵活,节省空间,因为使用时才载入,而不用时释放,同时显示调用不需要.lib库之类。

 

四、动态库的测试

   动态库写好之后,如何测试呢,在这介绍一种比较简单的方法,在动态库的工程中,按F5将出现以下界面:

意思是动态库是不能单独运行的,你得给它配一个主进程调用它,所以可以写一个简单的程序按上面调用动态库的方法调用动态库中想要测试的函数,编译通过后把exe文件的路径设置在上图的输入框中,点确定之后,再按F5,这时主进程就启动了,你在主进程中触发调用动态库函数的操作,这时将跳进动态库的工程中,动态库的工程处于调试状态,你可以一步一步跟踪,这样就能很好地测试动态库,并找出bug。

配套源码下载:http://download.csdn.net/detail/mingojiang/4475172

 

转载请注明出自:http://blog.csdn.net/mingojiang


0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:24874次
    • 积分:359
    • 等级:
    • 排名:千里之外
    • 原创:7篇
    • 转载:19篇
    • 译文:0篇
    • 评论:2条