windows下编写dll dll的优点 最简单的dll 显式调用dll Def文件方式定义dll的到处函数 显式调用dll 使用__declspec(dllexport)声明导出函数 关于 extern “C” 隐式调用DLL DLL中的全局变量和对象 小结 从其它语言调用DLL 在VC中编写DLL Visual Studio写dll windows下创建dll

windows下编写dll
dll的优点
最简单的dll
显式调用dll
Def文件方式定义dll的到处函数
显式调用dll
使用__declspec(dllexport)声明导出函数
关于 extern “C”
隐式调用DLL
DLL中的全局变量和对象
小结
从其它语言调用DLL
在VC中编写DLL
Visual Studio写dll
windows下创建dll

简单的说,dll有以下几个优点:

  1. 节省内存。同一个软件模块,若是以源代码的形式重用,则会被编译到不同的可执行程序中,同时运行这些exe时这些模块的二进制码会被重复加载到内存中。如果使用dll,则只在内存中加载一次,所有使用该dll的进程会共享此块内存(当然,像dll中的全局变量这种东西是会被每个进程复制一份的)。
  2. 不需编译的软件系统升级,若一个软件系统使用了dll,则该dll被改变(函数名不变)时,系统升级只需要更换此dll即可,不需要重新编译整个系统。事实上,很多软件都是以这种方式升级的。例如我们经常玩的星际、魔兽等游戏也是这样进行版本升级的。
  3. dll库可以供多种编程语言使用,例如用c编写的dll可以在vb中调用。这一点上dll还做得很不够,因此在dll的基础上发明了COM技术,更好的解决了一系列问题。要注意:com虽然也是以dll(或exe)的形式存在的,但它的调用方式却不同于普通dll。

最简单的dll

开始写dll之前,你需要一个c/c++编译器和链接器,并关闭你的IDE。是的,把你的VC和C++ BUILDER之类的东东都关掉,并打开你以往只用来记电话的记事本程序。不这样做的话,你可能一辈子也不明白dll的真谛。我使用了VC自带的cl编译器和link链接器,它们一般都在vc的bin目录下。(若你没有在安装vc的时候选择注册环境变量,那么就立刻将它们的路径加入path吧)如果你还是因为离开了IDE而害怕到哭泣的话,你可以关闭这个页面并继续去看《VC++技术内幕》之类无聊的书了。

最简单的dll并不比c的helloworld难,只要一个DllMain函数即可,包含objbase.h头文件(支持COM技术的一个头文件)。若你觉得这个头文件名字难记,那么用windows.h也可以。源代码如下:dll_nolib.cpp

BOOL APIENTRY DllMain(HANDLE hModule, DWORD dwReason, void* lpReserved){ 
    HANDLE g_hModule; 
    switch(dwReason) 
    { 
    case DLL_PROCESS_ATTACH: 
        cout<<"Dll is attached!"<<endl; 
        g_hModule = (HINSTANCE)hModule; 
        break; 
    case DLL_PROCESS_DETACH: 
        cout<<"Dll is detached!"<<endl; 
        g_hModule=NULL; 
        break; 
    } 
    return true; 
}

其中DllMain是每个dll的入口函数,如同c的main函数一样。DllMain带有三个参数,hModule表示本dll的实例句柄(听不懂就不理它,写过windows程序的自然懂),dwReason表示dll当前所处的状态,例如DLL_PROCESS_ATTACH表示dll刚刚被加载到一个进程中,DLL_PROCESS_DETACH表示dll刚刚从一个进程中卸载。当然还有表示加载到线程中和从线程中卸载的状态,这里省略。最后一个参数是一个保留参数(目前和dll的一些状态相关,但是很少使用)。

从上面的程序可以看出,当dll被加载到一个进程中时,dll打印"Dll is attached!"语句;当dll从进程中卸载时,打印"Dll is detached!"语句。

编译dll需要以下两条命令:cl /c dll_nolib.cpp
这条命令会将cpp编译为obj文件,若不使用/c参数则cl还会试图继续将obj链接为exe,但是这里是一个dll,没有main函数,因此会报错。不要紧,继续使用链接命令。
Link /dll dll_nolib.obj
这条命令会生成dll_nolib.dll。

注意,因为编译命令比较简单,所以本文不讨论nmake,有兴趣的可以使用nmake,或者写个bat批处理来编译链接dll。

显式调用dll

使用dll大体上有两种方式,显式调用和隐式调用。这里首先介绍显式调用。编写一个客户端程序:dll_nolib_client.cpp

#include <windows.h>
#include <iostream.h>
int main(void){ 
    //加载我们的dll 
    HINSTANCE hinst=::LoadLibrary("dll_nolib.dll"); 
    if (NULL != hinst) 
    { 
        cout<<"dll loaded!"<<endl; 
    } 
    return 0; 
}

注意,调用dll使用LoadLibrary函数,它的参数就是dll的路径和名称,返回值是dll的句柄。 使用如下命令编译链接客户端:Cl dll_nolib_client.cpp

并执行dll_nolib_client.exe,得到如下结果:

Dll is attached!
dll loaded!
Dll is detached!

以上结果表明dll已经被客户端加载过。但是这样仅仅能够将dll加载到内存,不能找到dll中的函数。我们可以使用dumpbin命令查看DLL中的函数。
Dumpbin命令可以查看一个dll中的输出函数符号名,键入如下命令:Dumpbin –exports dll_nolib.dll
通过查看,发现dll_nolib.dll并没有输出任何函数。
那么如何在dll中定义输出函数呢?总体来说有两种方法,一种是添加一个def定义文件,在此文件中定义dll中要输出的函数;第二种是在源代码中待输出的函数前加上__declspec(dllexport)关键字。

Def文件方式定义dll的到处函数

首先写一个带有输出函数的dll,源代码如下:dll_def.cpp

#include <objbase.h>
#include <iostream.h>
void FuncInDll (void){ 
    cout<<"FuncInDll is called!"<<endl; 
}
BOOL APIENTRY DllMain(HANDLE hModule, DWORD dwReason, void* lpReserved){ 
    HANDLE g_hModule; 
    switch(dwReason) 
    { 
    case DLL_PROCESS_ATTACH: 
        g_hModule = (HINSTANCE)hModule; 
        break; 
    case DLL_PROCESS_DETACH: 
        g_hModule=NULL; 
        break; 
    } 
    return TRUE; 
}

这个dll的def文件如下:dll_def.def

;
; dll_def module-definition file
;
LIBRARY         dll_def.dll
DESCRIPTION     '(c)2007-2009 Wang Xuebin'
EXPORTS
                FuncInDll @1 PRIVATE

你会发现def的语法很简单,首先是LIBRARY关键字,指定dll的名字;然后一个可选的关键字DESCRIPTION,后面写上版权等信息(不写也可以);最后是EXPORTS关键字,后面写上dll中所有要输出的函数名或变量名,然后接上@以及依次编号的数字(从1到N),最后接上修饰符。

用如下命令编译链接带有def文件的dll:

Cl /c dll_def.cpp
Link /dll dll_def.obj /def:dll_def.def

再调用dumpbin查看生成的dll_def.dll:Dumpbin –exports dll_def.dll
得到如下结果:

Dump of file dll_def.dll
File Type: DLL
Section contains the following exports for dll_def.dll
           0 characteristics
    46E4EE98 time date stamp Mon Sep 10 15:13:28 2007
        0.00 version
           1 ordinal base
           1 number of functions
           1 number of names
    ordinal hint RVA      name
          1    0 00001000 FuncInDll
Summary
        2000 .data
        1000 .rdata
        1000 .reloc
        6000 .text

观察这一行0 00001000 FuncInDll,会发现该dll输出了函数FuncInDll。

显式调用dll

写一个dll_def.dll的客户端程序:dll_def_client.cpp

#include <windows.h>
#include <iostream.h>
int main(void){ 
    //定义一个函数指针 
    typedef void (* DLLWITHLIB )(void); 
    //定义一个函数指针变量 
    DLLWITHLIB pfFuncInDll = NULL; 
    //加载我们的dll 
    HINSTANCE hinst=::LoadLibrary("dll_def.dll"); 
    if (NULL != hinst) 
    { 
        cout<<"dll loaded!"<<endl; 
    } 
    //找到dll的FuncInDll函数 
    pfFuncInDll = (DLLWITHLIB)GetProcAddress(hinst, "FuncInDll"); 
    //调用dll里的函数 
    if (NULL != pfFuncInDll) 
    { 
        (*pfFuncInDll)();   
    } 
    return 0; 
}

有两个地方值得注意,第一是函数指针的定义和使用,不懂的随便找本c++书看看;第二是GetProcAddress的使用,这个API是用来查找dll中的函数地址的,第一个参数是DLL的句柄,即LoadLibrary返回的句柄,第二个参数是dll中的函数名称,即dumpbin中输出的函数名(注意,这里的函数名称指的是编译后的函数名,不一定等于dll源代码中的函数名)。
编译链接这个客户端程序,并执行会得到:

dll loaded!
FuncInDll is called!

这表明客户端成功调用了dll中的函数FuncInDll。

使用__declspec(dllexport)声明导出函数

为每个dll写def显得很繁杂,目前def使用已经比较少了,更多的是使用__declspec(dllexport)在源代码中定义dll的输出函数。
Dll写法同上,去掉def文件,并在每个要输出的函数前面加上声明__declspec(dllexport),例如:

__declspec(dllexport) void FuncInDll (void)

这里提供一个dll源程序dll_withlib.cpp,然后编译链接。链接时不需要指定/DEF:参数,直接加/DLL参数即可,

Cl /c dll_withlib.cpp
Link /dll dll_withlib.obj

然后使用dumpbin命令查看,得到:

1    0 00001000 ?FuncInDll@@YAXXZ

可知编译后的函数名为?FuncInDll@@YAXXZ,而并不是FuncInDll,这是因为c++编译器基于函数重载的考虑,会更改函数名,这样使用显式调用的时候,也必须使用这个更改后的函数名,这显然给客户带来麻烦。为了避免这种现象,可以使用extern “C”指令来命令c++编译器以c编译器的方式来命名该函数。修改后的函数声明为:

extern "C" __declspec(dllexport) void FuncInDll (void)

dumpbin命令结果:

1    0 00001000 FuncInDll

这样,显式调用时只需查找函数名为FuncInDll的函数即可成功。

关于 extern “C”

使用extern “C”关键字实际上相当于一个编译器的开关,它可以将c++语言的函数编译为c语言的函数名称。即保持编译后的函数符号名等于源代码中的函数名称。
虽然使用 extern C 的方法可以解决名字改编的问题,但它有两个缺陷,一个是它不能用来导出一个类的成员函数,只能导出全局函数,使全局函数的名字不发生改变。另一个是如果我们导出的函数的调用约定发生改变的话,即使我们的函数用 extern C 作声明,函数名仍会发生改变。
调用约定,一般没有指定下,用 C 编译器一般默认就是 C 调用约定(_cdcel),常用的调用约定有以下几种:

  • _stdcall:也称 pascal 语言调用约定,在MS C++ 系列中的C/C++编译器,常用宏PASCAL宏来声明这个调用约定。
  • _cecel
  • _fastcal
  • _thiscall
  • _naked call

隐式调用DLL

显式调用显得非常复杂,每次都要LoadLibrary,并且每个函数都必须使用GetProcAddress来得到函数指针,这对于大量使用dll函数的客户是一种困扰。而隐式调用能够像使用c函数库一样使用dll中的函数,非常方便快捷。
下面是一个隐式调用的例子:dll包含两个文件dll_withlibAndH.cpp和dll_withlibAndH.h。
代码如下:dll_withlibAndH.h

extern "C" __declspec(dllexport) void FuncInDll (void);

dll_withlibAndH.cpp

#include <objbase.h>
#include <iostream.h>
#include "dll_withLibAndH.h"//看到没有,这就是我们增加的头文件
extern "C" __declspec(dllexport) void FuncInDll (void){ 
    cout<<"FuncInDll is called!"<<endl; 
}
BOOL APIENTRY DllMain(HANDLE hModule, DWORD dwReason, void* lpReserved){ 
    HANDLE g_hModule; 
    switch(dwReason) 
    { 
    case DLL_PROCESS_ATTACH: 
        g_hModule = (HINSTANCE)hModule; 
        break; 
    case DLL_PROCESS_DETACH: 
        g_hModule=NULL; 
        break; 
    } 
    return TRUE; 
}

编译链接命令:

Cl /c dll_withlibAndH.cpp
Link /dll dll_withlibAndH.obj

在进行隐式调用的时候需要在客户端引入头文件,并在链接时指明dll对应的lib文件(dll只要有函数输出,则链接的时候会产生一个与dll同名的lib文件)位置和名称。然后如同调用api函数库中的函数一样调用dll中的函数,不需要显式的LoadLibrary和GetProcAddress。使用最为方便。客户端代码如下:dll_withlibAndH_client.cpp

#include "dll_withLibAndH.h"
//注意路径,加载 dll的另一种方法是 Project | setting | link 设置里
#pragma comment(lib,"dll_withLibAndH.lib")
int main(void){ 
    FuncInDll();//只要这样我们就可以调用dll里的函数了 
    return 0; 
}

__declspec(dllexport)__declspec(dllimport)配对使用
上面一种隐式调用的方法很不错,但是在调用DLL中的对象和重载函数时会出现问题。因为使用extern “C”修饰了输出函数,因此重载函数肯定是会出问题的,因为它们都将被编译为同一个输出符号串(c语言是不支持重载的)。

事实上不使用extern “C”是可行的,这时函数会被编译为c++符号串,例如(?FuncInDll@@YAXH@Z、 ?FuncInDll@@YAXXZ),当客户端也是c++时,也能正确的隐式调用。

这时要考虑一个情况:若DLL1.CPP是源,DLL2.CPP使用了DLL1中的函数,但同时DLL2也是一个DLL,也要输出一些函数供Client.CPP使用。那么在DLL2中如何声明所有的函数,其中包含了从DLL1中引入的函数,还包括自己要输出的函数。这个时候就需要同时使用__declspec(dllexport)和__declspec(dllimport)了。前者用来修饰本dll中的输出函数,后者用来修饰从其它dll中引入的函数。

值得关注的是DLL1和DLL2中都使用的一个编码方法,见DLL2.H

#ifdef DLL_DLL2_EXPORTS
#define DLL_DLL2_API __declspec(dllexport)
#else
#define DLL_DLL2_API __declspec(dllimport)
#endif
DLL_DLL2_API void FuncInDll2(void);
DLL_DLL2_API void FuncInDll2(int);

在头文件中以这种方式定义宏DLL_DLL2_EXPORTS和DLL_DLL2_API,可以确保DLL端的函数用__declspec(dllexport)修饰,而客户端的函数用__declspec(dllimport)修饰。当然,记得在编译dll时加上参数/D “DLL_DLL2_EXPORTS”,或者干脆就在dll的cpp文件第一行加上#define DLL_DLL2_EXPORTS。

DLL中的全局变量和对象

解决了重载函数的问题,那么dll中的全局变量和对象都不是问题了,只是有一点语法需要注意。如源代码所示:dll_object.h

#ifdef DLL_OBJECT_EXPORTS
#define DLL_OBJECT_API __declspec(dllexport)
#else
#define DLL_OBJECT_API __declspec(dllimport)
#endif
DLL_OBJECT_API void FuncInDll(void);
extern DLL_OBJECT_API int g_nDll;
class DLL_OBJECT_API CDll_Object { 
public: 
    CDll_Object(void); 
    show(void); 
    // TODO: add your methods here. 
};

Cpp文件dll_object.cpp如下:

#define DLL_OBJECT_EXPORTS
#include <objbase.h>
#include <iostream.h>
#include "dll_object.h"
DLL_OBJECT_API void FuncInDll(void){ 
    cout<<"FuncInDll is called!"<<endl; 
}
DLL_OBJECT_API int g_nDll = 9;
CDll_Object::CDll_Object(){ 
    cout<<"ctor of CDll_Object"<<endl; 
}
CDll_Object::show(){ 
    cout<<"function show in class CDll_Object"<<endl; 
}
BOOL APIENTRY DllMain(HANDLE hModule, DWORD dwReason, void* lpReserved){ 
    HANDLE g_hModule; 
    switch(dwReason) 
    { 
    case DLL_PROCESS_ATTACH: 
        g_hModule = (HINSTANCE)hModule; 
        break; 
    case DLL_PROCESS_DETACH: 
        g_hModule=NULL; 
        break; 
    } 
    return TRUE; 
}

编译链接完后Dumpbin一下,可以看到输出了5个符号:

1    0 00001040 ??0CDll_Object@@QAE@XZ


2    1 00001000 ??4CDll_Object@@QAEAAV0@ABV0@@Z


3    2 00001020 ?FuncInDll@@YAXXZ


4    3 00008040 ?g_nDll@@3HA


5    4 00001069 ?show@CDll_Object@@QAEHXZ

它们分别代表类CDll_Object,类的构造函数,FuncInDll函数,全局变量g_nDll和类的成员函数show。下面是客户端代码:dll_object_client.cpp

#include "dll_object.h"
#include <iostream.h>
//注意路径,加载 dll的另一种方法是 Project | setting | link 设置里
#pragma comment(lib,"dll_object.lib")
int main(void){ 
    cout<<"call dll"<<endl; 
    cout<<"call function in dll"<<endl; 
    FuncInDll();//只要这样我们就可以调用dll里的函数了 
    cout<<"global var in dll g_nDll ="<<g_nDll<<endl; 
    cout<<"call member function of class CDll_Object in dll"<<endl; 
    CDll_Object obj; 
    obj.show(); 
    return 0; 
}

运行这个客户端可以看到:

call dll
call function in dll
FuncInDll is called!
global var in dll g_nDll =9
call member function of class CDll_Object in dll
ctor of CDll_Object
function show in class CDll_Object

可知,在客户端成功的访问了dll中的全局变量,并创建了dll中定义的C++对象,还调用了该对象的成员函数。

小结

牢记一点,说到底,DLL是对应C语言的动态链接技术,在输出C函数和变量时显得方便快捷;而在输出C++类、函数时需要通过各种手段,而且也并没有完美的解决方案,除非客户端也是c++。
记住,只有COM是对应C++语言的技术。
下面开始对各个问题一一小结。

显式调用和隐式调用

何时使用显式调用?何时使用隐式调用?我认为,只有一个时候使用显式调用是合理的,就是当客户端不是C/C++的时候。这时是无法隐式调用的。例如用VB调用C++写的dll。(VB我不会,所以没有例子)
我认为:显示调用更能节省内存,而且效率更高。所以在大型项目中应该多用显示调用的方法,下面从网上摘来一些更详细的说明:
静态调用方式所需的代码较动态调用方式所需的少,但存在着一些不足,一是如果要加载的DLL不存在或者DLL中没有要引入的例程,这时候程序就自动终止运行;二是 DLL一旦加载就一直驻留在应用程序的地址空间,即使DLL已不再需要了。动态调用方式就可解决以上问题,它在需要用到DLL的时候才通过 LoadLibrary函数引入,用完后通过FreeLibrary函数从内存中卸载,而且通过调GetProcAddress函数可以指定不同的例程。最重要的是,如果指定的DLL出错,至多是API调用失败,不会导致程序终止。以下将通过具体的实例说明说明这调用方式的使用方法。 动态连接库最大的特点就是能节省磁盘空间.当多个进程共享同一个DLL的时候,内存中只有一个DLL的代码.通过映射来使各个进程得以调用.

调用DLL,首先需要将DLL文件映像到用户进程的地址空间中,然后才能进行函数调用,这个函数和进程内部一般函数的调用方法相同。Windows提供了两种将DLL映像到进程地址空间的方法:隐式调用(通过lib和头文件)和显式调用(只通过提供的dll文件)。下面对这两种方式在vc中如何调用做详细的说明:

1、隐式调用(也叫隐式链接)
这种方法需要DLL工程经编译产生的LIB文件,此文件中包含了DLL允许应用程序调用的所有函数的列表,当链接器发现应用程序调用了LIB文件列出的某个函数,就会在应用程序的可执行文件的文件映像中加入一些信息,这些信息指出了包含这个函数的DLL文件的名字。当这个应用程序运行时,也就是它的可执行文件被操作系统产生映像文件时,系统会查看这个映像文件中关于DLL的信息,然后将这个DLL文件映像到进程的地址空间。
系统通过DLL文件的名称,试图加载这个文件到进程地址空间时,它寻找DLL 文件的路径按照先后顺序如下:

  • 程序运行时的目录,即可执行文件所在的目录;
  • 当前程序工作目录
  • 系统目录:对于Windows95/98来说,可以调用GetSystemDirectory函数来得到,对于WindowsNT/2000来说,指的是32位Windows的系统目录,也可以调用GetSystemDirectory函数来得到,得到的值为SYSTEM32
  • Windows目录
  • 列在PATH环境变量中的所有目录

VC中加载DLL的LIB文件的方法有以下三种:
①LIB文件直接加入到工程文件列表中
在VC中打开 File View 一页,选中工程名,单击鼠标右键,然后选中 Add Files to Project 菜单,在弹出的文件对话框中选中要加入DLL的LIB文件即可。
②设置工程的 Project Settings 来加载DLL的LIB文件
打开工程的 Project Settings菜单,选中Link,然后在Object/library modules下的文本框中输入DLL的LIB文件。
③通过程序代码的方式
加入预编译指令#pragma comment (lib,”*.lib”),这种方法优点是可以利用条件预编译指令链接不同版本的LIB文件。因为,在Debug方式下,产生的LIB文件是Debug版本,如Regd.lib;在Release方式下,产生的LIB文件是Release版本,如Regr.lib。

当应用程序对DLL的LIB文件加载后,还需要把DLL对应的头文件(*.h)包含到其中,在这个头文件中给出了DLL中定义的函数原型,然后声明。

2、显式调用(也叫显示加载)

隐式链接虽然实现较简单,但除了必须的.dll文件外还需要DLL的.h文件和.lib文件,在那些只提供.dll文件的场合就无法使用,而只能采用显式链接的方式。这种方式通过调用API函数来完成对DLL的加载与卸载,其能更加有效地使用内存,在编写大型应用程序时往往采用此方式。这种方法编程具体实现步骤如下:

①使用Windows API函数Load Library或者MFC提供的AfxLoadLibrary将DLL模块映像到进程的内存空间,对DLL模块进行动态加载。

②使用GetProcAddress函数得到要调用DLL中的函数的指针。

③不用DLL时,用Free Library函数或者AfxFreeLibrary函数从进程的地址空间显式卸载DLL。

Def和__declspec(dllexport)

其实def的功能相当于extern “C” __declspec(dllexport),所以它也仅能处理C函数,而不能处理重载函数。而__declspec(dllexport)__declspec(dllimport)配合使用能够适应任何情况,因此__declspec(dllexport)是更为先进的方法。所以,目前普遍的看法是不使用def文件,我也同意这个看法。

从其它语言调用DLL

从其它编程语言中调用DLL,有两个最大的问题,第一个就是函数符号的问题,前面已经多次提过了。这里有个两难选择,若使用extern “C”,则函数名称保持不变,调用较方便,但是不支持函数重载等一系列c++功能;若不使用extern “C”,则调用前要查看编译后的符号,非常不方便。

第二个问题就是函数调用压栈顺序的问题,即__cdecl和__stdcall的问题。__cdecl是常规的C/C++调用约定,这种调用约定下,函数调用后栈的清理工作是由调用者完成的。__stdcall是标准的调用约定,即这些函数将在返回到调用者之前将参数从栈中删除。

这两个问题DLL都不能很好的解决,只能说凑合着用。但是在COM中,都得到了完美的解决。所以,要在Windows平台实现语言无关性,还是只有使用COM中间件。

总而言之,除非客户端也使用C++,否则dll是不便于支持函数重载、类等c++特性的。DLL对c函数的支持很好,我想这也是为什么windows的函数库使用C加dll实现的理由之一。

在VC中编写DLL

在VC中创建、编译、链接dll是非常方便的,点击file → New → Project → Win32 Dynamic-Link Library,输入dll名称dll_InVC然后点击确定。然后选择A DLL that export some symbols,点击Finish。即可得到一个完整的DLL。

Visual Studio写dll

用Visual Sudio 6.0新建一个工程,工程的类型选择Win32 Dynamic-Link Library.工程名任意,其他所有选项取默认
新建一个cpp文件,代码如下:

int add(int a ,int b){
    return a+b;
}

如果工程类型是Win32 Console Application,那么在编译链接以后,会产生一个Debug目录,并且里面有一个exe文件
这里我们的工程类型是Win32 Dynamic-Link Library,在编译链接以后,我们期望产生一个Debug目录,并且里面有一个dll文件
事实正是如此
我们可以用depends工具打开这个dll文件以查看它导出了什么函数
depends工具在Tools菜单下.实际上它是D:Program FilesMicrosoft Visual StudioCommonTools下的一个文件
我们发现,这个dll没有导出任何东西
这是因为我们并没有说明我们要导出的东西.在那个cpp里的函数并不是默认会被导出的.因为它们可能只是被我们要导出的函数的调用的"内部函数".
要导出一个函数,我们须要加上_declspec(dllexport),代码变为:

int _declspec(dllexport) add(int a ,int b){
    return a+b;
}

再链接一次
再查看该dll文件,发现有一个?add@@YAHHH@Z的函数.好像很怪,不过总算看到东西了
现在来测试一下这个dll
新建一个工程,类型选Win32 Console Application
新建一个cpp文件,代码如下

#include <iostream.h>
#include <Windows.h>
void main(){
    typedef int (*ADD)(int ,int);//函数指针类型
    HINSTANCE Hint = ::LoadLibrary("DLL.dll");//加载我们刚才生成的dll
    ADD add = (ADD)GetProcAddress(Hint,"add");//取得dll导出的add方法
    cout<<add(3,4)<<endl;
}

其中LoadLibrary都是Windows.h里面声明了的函数
编译链接,都没问题
运行.出错了!
分析一下,程序怎么知道去哪里找我们的dll呢?
它会按如下顺序搜索:当前可执行模块所在的目录,当前目录, Windows 系统目录,Windows 目录。GetWindowsDirectory 函数检索此目录的路径,PATH 环境变量中列出的目录。
所以我们要把我们的dll复制一份到这个测试工程的Debug目录之后,再运行
还是出错了!
分析一下.我们刚才看到的是一个叫?add@@YAHHH@Z函数.那么,是不是这个原因呢?
把代码改为:

#include <iostream.h>
#include <Windows.h>
void main(){
    typedef int (*ADD)(int ,int);//函数指针类型
    HINSTANCE Hint = ::LoadLibrary("DLL.dll");//加载我们刚才生成的dll
    ADD add = (ADD)GetProcAddress(Hint,"?add@@YAHHH@Z");//取得dll导出的add方法
    cout<<add(3,4)<<endl;
}

再编译链接,运行,成功了!
那么怎么可以正确导出我们函数的名字呢?
在生成dll的工程的代码加上extern "C",改为:

extern "C" int _declspec(dllexport) add(int a ,int b)...{
    return a+b;
}

编译链接后,查看dll文件,可以看到导出的函数变为add了
这时下面代码可以正常工作了

#include <iostream.h>
#include <Windows.h>
void main()...{
    typedef int (*ADD)(int ,int);//函数指针类型
    HINSTANCE Hint = ::LoadLibrary("DLL.dll");//加载我们刚才生成的dll
    ADD add = (ADD)GetProcAddress(Hint,"add");//取得dll导出的add方法
    cout<<add(3,4)<<endl;
}

除了用_declspec(dllexport)指明要导出的函数,用extern "C"来纠正名字,我们还可用一个.def文件来达到以上目的
在dll工程里新建一个文件,类型选Text File,在名字要带上后缀.def
内容如下:

LIBRARY
EXPORTS
    add

剩下的步骤就和之前一样了
用def文件还可以改变导出的函数的名字,例如

LIBRARY
EXPORTS
    myadd = add

使得导出的函数叫myadd,而不是add
还可以给函数指定一个序号
如:

LIBRARY
EXPORTS
myadd=add @4

给myadd指定了一个序号
在测试工程里,可以根据序号取得我们的函数:

#include <iostream.h>
#include <Windows.h>
void main(){
    typedef int (*ADD)(int,int);
    HINSTANCE hInstance=::LoadLibrary("DLL.dll");
    ADD add=(ADD)GetProcAddress(hInstance,MAKEINTRESOURCE(4));//根据序号取得函数
    cout<<add(3,4)<<endl;
    add=(ADD)GetProcAddress(hInstance,"myadd");//在def文件里指定的名字
    cout<<add(3,4)<<endl;
    FreeLibrary(hInstance);//释放加载了的dll文件占的资源
}

以上讲的是运行时动态加载dll,下面讲启动时动态加载dll

产生dll的工程不用变,还是上面这个(名字是myadd,序号为4)
测试代码改为:

//先把DLL.lib文件复制到本工程目录里

#include <iostream.h>
#pragma comment(lib,"DLL.lib")
extern int myadd(int ,int );//没有加这句而只加上面这句(或在工程设置里加上DLL.lib)会链接错误
void main()
{
    cout<<myadd(3,4)<<endl;
}

这种方法调用dll,在链接的时候,会在我们exe里包含要引用的符号,在启动程序的时候就会加载所有需要的dll.(之前说错了,说这是静态链接)
#pragma comment(lib,"DLL.lib")指明了用到哪个dll,其中DLL.lib可以在Debug找到.我们也要把DLL.lib复制到测试工程目录(不是Debug目录).我们也可以在工程属性里添加.方法是Project--Settings--Link,在Object/libraries Modules最后加上 DLL.lib
extern int add(int ,int )指明了我们的add是一个外部函数,而不是在本文件定义的

最后,强调一下,要把该复制的文件复制到正确的地方.
当你产生的dll文件和我说的不一致时,试一下选Build-Rebuild All

windows下创建dll

编写代码

建立项目,请选择Win32 控制台项目(Win32 Console Application),选择dll和空项目选项。
首先写头文件(header file),称为dllTutorial.h。这个文件与其它头文件一样,其中只是一些函数的原型。

#ifndef _dll_TUTORIAL_H_  
#define _dll_TUTORIAL_H_  
#include <iostream>  
  
#if defined dll_EXPORT  
#define DECLDIR __declspec(dllexport)  
#else  
#define DECLDIR __declspec(dllimport)  
#endif  
  
extern "C"  
{  
        DECLDIR int Add( int a, int b );  
        DECLDIR void Function( void );  
}  
#endif  

此代码中,前面两行指示编译器只包含这个文件一次。extern "C"告诉编译器该部分可以在C/C++中使用。
在VC++中这里有两个方法来导出函数:
1、使用__declspec,一个Microsoft定义的关键字。
2、创建一个模块定义文件(Module-Definition File即.DEF)。

第一种方法稍稍比第二种方法简单些,但两种都工作得很好。

接下来实现dll_Tutorial.cpp文件

#include <iostream>  
#include "dll_Tutorial.h"  
  
#define dll_EXPORT  
  
extern "C"  
{  
        DECLDIR int Add( int a, int b )  
        {  
          return( a + b );  
        }  
  
       DECLDIR void Function( void )  
        {  
          std::cout << "dll Called!" << std::endl;  
        }  
  
}  

模块定义文件(.def)

模块定义文件是一个有着.def文件扩展名的文本文件。它被用于导出一个dll的函数,和__declspec(dllexport)很相似,但是.def文件并不是Microsoft定义的。一个.def文件中只有两个必需的部分:LIBRARY 和 EXPORTS。让我们先看一个基本的.def文件稍后我将解析之。

LIBRARY dll_tutorial
DESCRIPTION "our simple dll"
EXPORTS
        Add @1
        Function @2

第一行,LIBRARY是一个必需的部分。它告诉链接器(linker)如何命名你的dll。下面被标识为''DESCRIPTION''的部分并不是必需的,但是我喜欢把它放进去。该语句将字符串写入 .rdata 节,它告诉人们谁可能使用这个dll,这个dll做什么或它为了什么(存在)。再下面的部分标识为EXPORTS是另一个必需的部分;这个部分使得该函数可以被其它应用程序访问到并且它创建一个导入库。此外,它还有其它四个部分:NAME, STACKSIZE, SECTIONS, 和VERSION。

使用隐式链接的方式调用生成的dll

创建一个新的空的Win32控制台项目并添加一个源文件,将生成的dll放入你的新项目相同的目录下。
链接到一个dll有两种方式:

  1. 在IDE中指明
  2. 使用#pragma comment(lib, "dllTutorial.lib")语句替代
#include <iostream>  
#include <dllTutorial.h>  

int main()  
{  
        Function();  
        std::cout << Add(32, 58) << "/n";  
        return(1);  
}  

使用显示链接的方式调用生成的dll

显示链接加载dll的方式不需要引入dll所对应的头文件,而仅需一个dll文件即可。

#include <iostream>  
#include <windows.h>  
  
   
typedef int (*AddFunc)(int,int);  
typedef void (*FunctionFunc)();  
  
int main()  
{  
        AddFunc _AddFunc;  
        FunctionFunc _FunctionFunc;  
        HINSTANCE hInstLibrary = LoadLibrary("dll_Tutorial.dll");  
  
        if (hInstLibrary == NULL)  
        {  
        FreeLibrary(hInstLibrary);  
        }  
  
        _AddFunc = (AddFunc)GetProcAddress(hInstLibrary, "Add");  
  
  
  
        _FunctionFunc = (FunctionFunc)GetProcAddress(hInstLibrary, "Function");  
  
       if ((_AddFunc == NULL) || (_FunctionFunc == NULL))  
        {  
        FreeLibrary(hInstLibrary);  
        }  
  
        std::cout << _AddFunc(23, 43) << std::endl;  
        _FunctionFunc();  
  
        std::cin.get();  
  
        FreeLibrary(hInstLibrary);  
  
        return(1);  
 }  

在此文件中,开头进行了函数声明。

typedef int (*AddFunc)(int,int);
typedef void (*FunctionFunc)();

一个HINSTANCE是一个Windows数据类型:是一个实例的句柄;在此情况下,这个实例将是这个dll,可以通过使用函数LoadLibrary()获得dll的实例,它获得一个名称作为参数。在调用LoadLibrary函数后,必须查看一下函数返回是否成功,这可以通过检查HINSTANCE是否等于NULL(在Windows.h中定义为0或Windows.h包含的一个头文件)来实现。如果其等于NULL,该句柄将是无效的,需要及时释放这个库。换句话说,必须释放dll获得的内存。如果函数返回成功,你的HINSTANCE就包含了指向dll的句柄。

一旦你获得了指向dll的句柄,就可以从dll中重新获得函数。为了这样做,你必须使用函数GetProcAddress(),它将dll的句柄(可以使用HINSTANCE)和函数的名称作为参数,可以让函数指针获得由GetProcAddress()返回的值,同时必须将GetProcAddress()转换为那个函数定义的函数指针。例如,对于Add()函数,需要将GetProcAddress()转换为AddFunc。

一旦函数指针拥有dll的函数,你现在就可以使用它们了,但是这里有一个需要注意的地方:你不能使用函数的实际名称;你必需使用函数指针来调用它们。在那以后,所有你需要做的是释放库。