DLL的创建和使用

来源:互联网 发布:cisco 三层端口ip配置 编辑:程序博客网 时间:2024/05/21 09:56

DLL的创建和使用

参考:http://blog.csdn.net/btwsmile/article/details/6676802

           http://blog.csdn.net/hjsunj/article/details/2047376

         http://blog.csdn.net/wujian53/article/details/706975
一、为什么需要dll

代码复用是提高软件开发效率的重要途径。一般而言,只要某部分代码具有通用性,就可将它构造成相对独立的功能模块

并在之后的项目中重复使用。比较常见的例子是各种应用程序框架,如ATL、MFC等,它们都以源代码的形式发布。由于

这种复用是“源码级别”的,源代码完全暴露给了程序员,因而称之为“白盒复用”。

“白盒复用”的缺点比较多,总结起来有4点。

1.暴露了源代码;
2.容易与程序员的“普通”代码发生命名冲突;
3.多份拷贝,造成存储浪费;
4.更新功能模块比较困难。
实际上,以上4点概括起来就是“暴露的源代码”造成“代码严重耦合”。为了弥补这些不足,提出了“二进制级别”的代码复用。

使用二进制级别的代码复用一定程度上隐藏了源代码,对于缓解代码耦合现象起到了一定的作用。这样的复用被称为“黑盒

复用”。在Windows操作系统中有两种可执行文件,其后缀名分别为.exe和.dll。它们的区别在于,.exe文件可被独立的装载于

内存中运行;.dll文件却不能,它只能被其它进程调用。然而无论什么格式,它们都是二进制文件。上面说到的“二进制级别”

的代码复用,可以使用.dll来实现。与白盒复用相比,.dll很大程度上弥补了上述4大缺陷。.dll是二进制文件,因此隐藏了源代

码;如果采用“显式调用”(后边将会提到),一般不会发生命名冲突;由于.dll是动态链接到应用程序中去的,它并不会在链

接生成程序时被原原本本拷贝进去;.dll文件相对独立的存在,因此更新功能模块是可行的。

说明:实现“黑盒复用”的途径不只dll一种,静态链接库甚至更高级的COM组件都是。本文只对dll进行讨论

二、静态库与DLL的不同之处

可执行文件的生成(Link期):前者很慢(因为要将库中的所有符号定义Link到EXE文件中),而后者很快(因为后者

    被Link的引入库文件无符号定义)可执行文件的大小:前者很大,后者很小(加上DLL的大小就和前者差不多了)

可执行文件的运行速度:前者快(直接在EXE模块的内存中查找符号),后者慢(需要在DLL模块的内存中查找,在另

    一个模块 的内存中查找自然较慢)
可共享性:前者不可共享,也就是说如果两个EXE使用了同一个静态库,那么实际在内存中存在此库的两份拷贝,而后

      者是可共享的。
可升级性:前者不可升级(因为静态库符号已经编入EXE中,要升级则EXE也需要重新编译),后者可以升级(只要接

口不变,  DLL即可被升级为不同的实现)
 
综合以上,选择静态库还是DLL
1. 静态库适于稳定的代码,而动态库则适于经常更改代码(当然接口要保持不变),当DLL更改(仅实现部分)后,

    用户不需要  重编工程,只需要使用新的Dll即可。
2. 由于静态库很吃可执行文件的生成(Link期)时间,所以如果对可执行文件的Link时间比较敏感,那么就用DLL。

三、使用DLL

1. 显式调用(也叫动态调用)
  显示调用使用API函数LoadLibrary或者MFC提供的AfxLoadLibrary将DLL加载到内存,再用GetProcAddress()在内存中获取引入函数地址,然后
  你就可以象使用本地函数一样来调用此引入函数了。在应用程序退出之前,应该用FreeLibrary或MFC提供的AfxLoadLibrary释放DLL。

  下面是个显示调用的例子,假定你已经有一个Test.dll,并且DLL中有个函数为void Test();

#include < iostream >using namespace std;typedef void(*TEST )();int main( char argc, char* argv[] ) {             const char* dllName = "Test.dll";    const char* funcName = "Test";    HMODULE hDLL = LoadLibrary( dllName );    if ( hDLL != NULL ) {        TEST func = TEST( GetProcAddress( hDLL, funcName ) );        if ( func != NULL ) {            func();        }        else {            cout << "Unable to find function /'" << funcName << "/' !" << endl;        }        FreeLibrary( hDLL );    }     else {        cout << "Unable to load DLL /'" << dllName << "/' !" << endl;    }        return 0;}


 

注意
1). 显示调用使用GetProcAddress,所以只能加载函数,无法加载变量和类。
2). 此外GetProcAddress是直接在.dll文件中寻找同名函数,如果DLL中的Test函数是个C++函数,那么由于在.dll文件中的
   实际文件名会被修饰(具体被修饰的规则可参考函数调用约定详解或者使用VC自带的Depends.exe查看),所以直接找Test
   是找不到的,这时必须把函数名修改为正确的被修饰后的名称,下面是一种可能(此函数在DLL中的调用约定为__cdecl):
  const char* funcName = "?Test@@YAXXZ";

2. 隐式调用(也叫静态调用)
   隐式调用必须提供DLL的头文件和引入库(可以看作轻量级的静态库(没有符号定义,但是说明了符号处于哪个DLL中))。
   有了头文件和引入库,DLL的使用就跟普通静态库的使用没啥区别,只除了DLL要和EXE一起发布。

   显示调用与隐式调用的优缺点

   显示调用使用复杂,但能更加有效地使用内存,因为DLL是在EXE运行时(run time)加载,必须由用户使用LoadLibrary和
   FreeLibrary来控制DLL从内存加载或卸载的时机。此外还可以加载其他语言编写的DLL函数。
   静态调用使用简单,但不能控制DLL加载时机,EXE加载到内存同时自动加载DLL到内存,EXE退出时DLL也被卸载

四、vs2010中创建DLL
1、File > New > Project  > Win32 > Win32 project,命名为MyDLL.
2、win32应用程序向导>下一步>勾选"DLL(D)"和"导出符号(X)",完成.
    (勾选"导出符号(X)"后,vs2010会自动生成MyDLL.h的头文件,主要包含如下内容:

 #ifdef MyDLL_EXPORTS    #define MyDLL_API __declspec(dllexport)    #else    #define MyDLL_API __declspec(dllimport)    #endif

 )

项目结构图:


3、在MyDLL.h中完成函数声明,在MyDLL.cpp中完成函数实现。此处写一个函数int add(int a,int b)为例。对DLL外部可见的函数,即DLL提供给外部调用的函数,
   需要在函数头部加上MyDLL_API(在MyDLL.h中宏定义好的),表示这个函数是可以被外部调用的。

   改好后的MyDLL.h如下:

// 下列 ifdef 块是创建使从 DLL 导出更简单的// 宏的标准方法。此 DLL 中的所有文件都是用命令行上定义的 MYDLL_EXPORTS// 符号编译的。在使用此 DLL 的// 任何其他项目上不应定义此符号。这样,源文件中包含此文件的任何其他项目都会将// MYDLL_API 函数视为是从 DLL 导入的,而此 DLL 则将用此宏定义的// 符号视为是被导出的。#ifdef MYDLL_EXPORTS#define MYDLL_API __declspec(dllexport)#else#define MYDLL_API __declspec(dllimport)#endifMYDLL_API int add(int,int);//函数声明

改好后 MyDLL.cpp如下:

// MyDLL.cpp : 定义 DLL 应用程序的导出函数。//#include "stdafx.h"#include "MyDLL.h"// 这是导出函数的一个示例。MYDLL_API int add(int a,int b){ return a+b;}


4、生成项目,在 项目目录\Debug文件下就生成了MyDLL.dll文件

  i、如果是C++函数
   此时使用MyDLL.dll,以add的名称来访问int add(int a,int b)函数时,很可能失败。原因是源码在编译生成二进制的DLL时,函数是个C++函数,
   那么由于在.dll文件中的实际函数名会被修饰,同时包含函数名和参数(以便于重载的实现)(具体被修饰的规则可参考函数调用约定详解或者

   使用VC自带的Depends.exe查看),所以直接找Test是找不到的,这时必须把函数名修改为正确的被修饰后的名称来访问,下面是一种可能的修饰后的名称):
  "?add@@YAHHH@Z"

 --使用VS2010附带工具dumpbin,查看CreateDLL.dll的导出函数名,结果如图所示:

ii、如果是C函数,可以正常调用

 虽然可以使用上面查到的DLL中的名字来调用,但很麻烦。
 简单的方法有:
 a、运用模块定义文件.def:

在项目中添加MyDLL.def文件:

内容为:
LIBRARY MyDLL
 EXPORTS
 add
 
   LIBRARY是模块定义文件必须的一部分,它告诉链接器(linker)如何命名你的DLL。EXPORTS也是模块定义文件必须的一部分,

   这部分使得该函 数可以被其它应用程序访问到并且它创建一个导入库。当你生成这个项目时,不仅是一个.dll文件被创建,而且一

   个文件扩展名为.lib的导出 库也会被创建。EXPORTS后面列出要导出函数的名称。可以在.def文件中的导出函数名后加@n,表示

   要导出函数的序号为n(对于VB、PB、Delphi用户,通常使用按名称进行调用的方式,这个数关系不大,但是对于使用.lib链接的

   VC程序来说,不是按名称进行调用,而是按照这个数进行调用的,所以最好给出。)

从新生成MyDLL.dll文件,查看:

此时就可以直接用源码中的名称调用这个函数了。

b运用extern  "C"修饰函数add(). (extern  "C" 作用是,让被其修饰的函数在编译好的文件(如.dll和.lib文件)中按照C语言的方式命名)