在VC中新建一个空的Win32动态链接库工程(Win32 Domanic Library),然后添加一个C++ Sourse File到工程,我这里的文件名取DllTest.cpp。然后在文件中添加如下内容: _declspec(dllexport) int add(int a,int b) { return a+b; } _declspec(dllexport) int subtract(int a,int b) { return a-b; } 接下来编译链接,就会在debug目录下生成一个调试版本的动态链接库,该链接库包含了add和subtract两个可供外部调用的函数。我们注意到,在源文件中多了一个没有见过的语句 declspec(dllexport) ,这个语句的作用就是向编译器指出我需要在生成的动态链接库中导出的函数,没有导出的函数是不能被其他程序调用的。要知道一个动态链接库导出了什么函数,可以在命令提示行用命令"dumpbin -exports DllTest.dll"来查看(也可以用VC工具包中的depends使用程序来查看)。以下是用dumpbin命令查看DllTest.dll而生成的信息:
Section contains the following exports for DllTest.dll 0 characteristics 4420BEA4 time date stamp Wed Mar 22 11:04:04 2006 0.00 version 1 ordinal base 2 number of functions 2 number of names 1 0 0000100A ?add@@YAHHH@Z 2 1 00001005 ?subtract@@YAHHH@Z 7000 .data 1000 .idata 3000 .rdata 2000 .reloc 2A000 .text 可以看到,我们编写的动态链接库导出了两个函数,分别名为?add@@YAHHH@Z 和 ?subtract@@YAHHH@Z,为什么名字不是add和subtract呢?这是因为 C++为了支持函数的重载,会在编译时将函数的参数类型信息以及返回值类型信息加入到函数名中,这样代码中名字一样的重载函数,在经过编译后就互相区分开了,调用时 函数名也经过同样的处理,就能找到对应的函数了。编译器对函数的重命名规则是与调用方式相关的,在这里采用的是C++的默认调用方式。以此对应的还有stdcall方式、 cdecl方式、fastcall方式和thiscall方式,不同调用方式的重命名规则不一样。 需要特别说一下的是stdcall方式和cdecl方式: stdcall方式(标准调用方式)也即pascal调用方式,它的重命名规则是函数名自动加前导的下划线,后面紧跟一个@符号,其后紧跟着参数所占字节数,之所以要跟参数字节数,是因为stdcall采用被调函数平衡堆栈方式,用函数名最后的数字告诉编译器需要为函数平衡的字节数。例如,如果我们的DllTest.dll采用stdcall方式编译的话,导出的函数名将会是 _add@8 和 _subtract@8 ,而函数编译后的汇编代码最后一句一定是 ret8。 cdecl方式即C语言调用方式,它的重命名规则仅仅是在函数名前加下划线(奇怪的是我用vc6编译的c语言函数,名字没有任何改变),因为C语言采用的是调用函数平衡堆栈的方式,所以不需要在函数名中加入参数所占的字节数,这样的堆栈平衡方式也使C语言可以编写出参数不固定的函数;同时C语言不支持函数重载,因此不需要在函数名中加入参数类型信息和返回值类型信息。 动态链接库已经生成了,接下来就是调用的工作了。调用动态链接库有两种方式:隐式调用和显式调用,下面我们分别来看两种调用方式的具体过程:
新建一个空的Win32 Console Application,命名为DllCaller,向工程中添加名为DllCaller.cpp 的C++ Sourse File,在文件中写入如下代码:
#include <iostream>using namespace std; (1)extern int add(int a,int b); //第一种调用方式(2)_declspec(dllimport) int add(int a,int b);//第二种调用方式
int main(){ cout<<"3+5="<<add(3,5)<<endl;
编译,没有错误,链接,有两个错误:找不到外部引用符号。要怎样才能让我们的程序找到动态连接库中的函数呢?这里是关键的一步。到刚才的DllTest工程目录下,从debug文件夹中拷贝生成的DllTest.dll文件和DllTest.lib文件到DllCaller工程目录。然后依次在vc中选择菜单:Project -->Settings-->Liink, 在Object/library Modules中加入一项文件名:DllTest.lib,这里的DllTest.lib并不是静态库文件,而是DllTest.dll的导入库文件,它包含了DllTest.dll动态链接库导出的函数信息,只有在工程链接设置里添加了该文件,才能够使调用了该动态链接库的工程正确链接。完成以上步骤后,我们再编译链接工程,这次没有任何错误!程序可以顺利调用动态连接库文件,正常运行了(为了能使程序找到并加载需要的动态链接库,动态链接库文件必须与调用程序在同一个目录下,或在path环境变量指定的目录下)。
这里需要说明一点,工程中的源文件在调用动态链接库中的函数时,需要提前声明,声名有两种方式,一种是传统的extern方式,一种是_declspec(dllimport)方式,这两种方式在代码中我都给出了。其中,第二种方式能使编译过程更快,所以推荐使用。 比起隐式调用,显示调用更加灵活,而且在编译链接时不需要lib导入库文件,也不需要提前声明函数。我们通过windows提供的API函数来动态加载动态连接库并调用其中的函数,用完后可以马上释放内存中的动态链接库,十分方便。下面就是显示调用动态链接库的代码: #include <iostream>#include <windows.h>using namespace std;
int main(){ HINSTANCE hInstance=LoadLibrary("DllTest.dll"); typedef int (*AddProc)(int,int); AddProc Add=(AddProc)GetProcAddress(hInstance,?add@@YAHHH@Z); if(!Add) { cout<<"动态连接库库函数未找到"<<endl; return 0; } cout<<"3+5="<<Add(3,5)<<endl; 以上代码并不复杂,首先定义一个实例句柄用来引用由Windows API 函数LoadLibrary加载的动态链接库,LoadLibrary函数的参数是一个字符串指针,具体调用时我们需要填入需要加载的动态链接库的位置及文件名,加载成功后返回一个实例句柄。接下来我们定义一个函数指针类型,用该类型声明一个函数指针,用来存储GetProcAddress函数返回的动态库函数入口地址。GetProcAddress能从指定的动态库中查找指定名字的函数,如果查找成功则返回该函数的入口地址,如果失败则返回NULL。更多GetProcAddress函数的用法请参看MSDN。有人可能注意到,GetProcAddress函数中指定的函数名并不是add,而是?add@@YAHHH@Z。这里就和前面将的函数调用方式联系起来了, 在GetProcAddress函数中,我们指定的函数名必须是编译后经过重命名的函数名,而不是源文件中定义的函数名 。这样实际上给我们的调用带来了相当大的麻烦,因为我们不可能去了解每一个经过重命名的导出函数名。好在微软已经给出了解决方法,那就是在编写动态链接库时同时编写一个以def为后缀的编译命名参考文件,如果动态链接库工程中有该文件,则编译器会根据该文件指定的函数名来导出动态库函数,关于def文件的详细使用方法请参考MSDN,这里就不一一赘述。找到需要的动态库函数后,我们就可以按需要对它进行调用,之后调用FreeLibrary函数释放动态库。因为动态库是多进程共享的,因此调用FreeLibrary函数并不意味着动态库在内存中被释放,每个动态库都有一个变量用来记录它的共享引用计数,而FreeLibrary的功能只是将这个记数减一,只有当一个动态库的引用计数为0时,它才会被操作系统释放。
前面已经详细介绍了动态链接库的两种调用方法,相比之下,隐式调用在编程时比较简单,指定导入库文件后,不必考虑函数的重命名,就可以直接调用动态库函数。但由于隐式调用不能指定动态库的加载时机,因此在一个程序开始运行时,操作系统会将该程序需要的动态链接库都加载入内存,势必造成程序初始化的时间过长,影响用户体验。而显式调用采用动态加载的方法,用到什么加载什么,用完即释放,灵活性较高,可以使程序得到优化。具体运用中到底采用哪种方法,还要依实际情况而定。