Windows API中所有的函数都包含在DLL中,其中最重要的三个DLL分别是HERNEL32.DLL User32.dll GDI32.dll
在使用动态链接库的时候,往往要提供两个文件:引入库(.lib)和DLL。引入库包含DLL导出的函数和变量的符号名,DLL中包含实际的函数和数据。编译链接可执行文件时,只需要链接引入库,DLL中的函数代码和数据并不复制到可执行文件中,在运行的时候,再去加载DLL,访问DLL中导出的函数。
加载的两种方式:隐式链接、显示加载
实例:
打开VC6.0 新建一个DLL工程

工程名为Dll1 点击OK 选择一个空的DLL工程 点击完成
然后新建一个C++源文件

文件名为Dll1
在文件中编写一下两个功能函数:
int add(int a,int b)
{
return a + b;
}
int subtract(int a,int b)
{
return a - b;
}
编译 链接 在工程的Debug目录下会生成一个Dll1.dll文件
这时这个DLL文件中的功能函数是不能访问的 因为没有被导出 可以用一下方式查看一个DLL文件中都是有哪些函数被导出
打开命令提示符窗口 切换到Dll1工程的Debug目录下 输入如下命令
dumpbin -exports Dll1.dll

在输入此命令之前,直接运行dumpbin命令 如果找不到该命令 则如下

在上图的目录下找到 VCVAS32.BAT文件 见这个文件直接拖拽到命令提示符窗口中 这时在这个命令提示符窗口下便可以用dumpbin命令了 打开新的命令提示符的时候 需要重复上面的操作才行。
怎么样才能将DLL文件中的函数导出呢?如下:
_declspec(dllexport) int add(int a,int b)
{
return a + b;
}
_declspec(dllexport) int subtract(int a,int b)
{
return a - b;
}
这样便声明了两个函数均导出
此时在编译链接 然后再命令提示符窗口下运行 dumpbin -exports Dll1.dll

如图可以看到 有两个函数被导出 但是这时的名字分别是 ?add@@YAHHH@Z和?subtract@@YAHHH@Z 这是VC6.0将函数名进行改编的结果 因为名字的改编,会设计到一些其他问题,后面再说如何处理。
现在 我们在新建一个工程 如下:

工程名为:Dll1Test 选择基于对话框的的工程

然后Finish 在对话框中添加两个按钮 如图:

将两个按钮的ID号分别修改为:IDC_BTN_ADD 和 IDC_BTN_SUBTRACT
双击两个按钮 分别添加处理函数
添加如下代码:

第一个红色的圈圈标出来的是导入这两个函数 第二个红圈圈提示有三个错误 这是为什么呢?因为我们没有将DLL文件和lib引入库文件添加进来
首先 要进行如下设置:
点开Project菜单 点击Settings 弹出如下对话框 切换到Link下 进行如下设置

再编译运行 还是有一次错误 那么 我们将Dll1这个工程中生成的在Debug目录下的Dll1.lib文件拷贝到Dll1Test这个工程目录下
再进行编译链接 这时候是没有错误的了 那么现在是否可以运行程序了呢?不行!!!为什么?因为我们还要讲第一个工程里面生成的Dll1.dll文件拷贝到Dll1Test这个工程目录下,否则是找不到Dll1.dll这个文件的。
然后编译链接,在点击运行之前,可以用一个工具查看一下我们这个exe程序运行所依赖的所有动态链接库
在命令提示符中,将目录切换到Dll1Test工程下的Debug目录下
然后运行如下命令 dumpbin -imports Dll1Test.exe

显示出来的是Dll1Test.exe运行所需要的所有的动态链接库文件信息
最后,我们点击运行,就可以得到如下的结果了

关于动态链接库中头文件的使用
在编写一个给用户使用的动态链接库的时候,通常还要编写一个头文件 这个头文件包含了动态链接库中导出函数的声明原型 然后让客户程序包含该头文件即可。头文件内容如下:
_declspec(dllimport) int add(int a,int b);
_declspec(dllimport) int subtract(int a,int b);
如果动态链接库自身也想使用该头文件 需要对头文件进行改编一下,如下:
#ifdef DLL1_API
#else
#define DLL1_API _declspec(dllimport)
#endif
DLL1_API int add(int a,int b);
DLL1_API int subtract(int a,int b);
然后修改动态链接库源文件:
#define DLL1_API _declspec(dllimport)
#include "Dll1.h"
int add(int a,int b)
{
return a + b;
}
int subtract(int a,int b)
{
return a - b;
}
动态链接库中类成员函数的导出
想要导出动态链接库中的类,如下:
class DLL1_API Point
{
public:
void output(int x,int y);
};
其中,类中的成员函数和变量的访问属性不变。
下面通过一个例子说明类成员函数的使用。
在Dll1工程源文件中添加如下代码:
#include <windows.h> //因为用到了windows系统API
#include <stdio.h> //用到了标准输出函数
void Point::output(int x,int y)
{
HWND hwnd = GetForegroundWindow(); //要在使用该动态链接库的客户端程序中使用该函数,首先要获取客户端程序的句柄,可以用GetForegroundWindow()函数来获取,然后定义一个HWND类型的变量 保存客户端程序的句柄
HDC hdc = GetDC(hwnd);//通过句柄得到DC
char buf[20]; //存放显示信息的buffer
memset(buf,0,20);
sprintf(buf,"x=%d,y=%d",x,y);
TextOut(hdc,0,0,buf,strlen(buf));//调用文本输出函数,将字符串信息输出到客户端程序中
ReleaseDC(hwnd,hdc); //释放客户端程序的句柄
}
将编译连接之后生成的DLL文件和LIB文件拷贝到Dll1Test测试工程目录下
然后再测试工程的对话框中添加一个按钮output 并且将IDC该为IDC_BTN_OUTPUT
双击该按钮,生成相应函数,在函数中添加如下代码:
Point pt;
pt.output(5,3);
在编译链接之前,将我们前面在工程Dll1中编写的Dll1.h头文件拷贝到Dll1Test工程目录下面,并且在Dll1Test工程的源文件中包含上此头文件 然后编译链接运行,点击output按钮,得到如下图所示的结果:

如果不想导出整个类,只导出类中的某些成员函数 只需要把class后面的DLL1_API去掉,然后添加到要导出的成员函数的声明的前面即可。
导出函数名字改编问题
C++编译器编写的动态链接库会修改动态链接库中函数名字,上面提到了函数名改编的问题。
这时候如果用其他编译器编写一个客户端,然后使用动态链接库,就会出错,找不到相应的函数,因为不同的编译器函数名改编是不一样的。
一种方法是防止编译器进行函数名改编。
在Dll1工程中,将源文件做些改动:
#define DLL1_API extern "C" _declspec(dllexport)
然后先将关于类方面的代码注释掉 编译链接 然后同样用dumpbin工具对Dll1.dll文件进行查看:

发现这个时候 函数的名字没有进行改编 在没有加extern “C”之前是
?add@@YAHHH@Z ?subtract@@YAHHH@Z
与此同时,在Dll1.h这个头文件中同样需要添加 extern “C” 因为客户端包含此头文件时,如果不加入extern “C”,客户端还是会根据C++编译器的名字改编来访问函数,但是在DLL源文件中我们加入了extern “C”,因此名字没有被改编,所以不一致。
然而,用extern “C”的缺点是:
一、只能用来导出全局函数,不能导出类的成员函数。
二、如果所导出的函数的调用约定发生改变,即使加上extern “C”,函数名仍然会被改编。
举例,在Dll1工程中,将两个函数的调用约定该为标准调用约定的
在头文件中如下修改:
DLL1_API int _stdcall add(int a,int b);
DLL1_API int _stdcall subtract(int a,int b);
在源文件中也进行修改:
int _stdcall add(int a,int b)
{
return a + b;
}
int _stdcall subtract(int a,int b)
{
return a - b;
}
这样,这两个导出函数的调用约定就改成了标准调用约定,标准调用约定是WindowsAPI的调用约定,和C语言的调用约定是不一样的。
这个时候,编译链接一下,然后我们再次到命令提示符窗口下,同样用dumpbin工具对此时生成的Dll1.dll进行查看
如图
这时,我们发现,导出函数的名字又被改编了 _add@8 和 _substract@8 其中@后面的8表示函数的参数类型占用8个字节
因为不同平台和语言编写的程序的调用约定是不一样的,比我我们用Delphi编写的客户端,调用C语言开发的动态链接库的时候,就会出现名字改编引起的调用错误,怎么样解决呢?这就要引入一个“模块定义文件”来解决。
关闭Dll1工程,新建一个Win32 DLL工程,工程名为Dll2 和Dll1一样进行相应的操作,然后创建一个C++源文件,在源文件中写两个函数
int add(int a,int b)
{
return a + b;
}
int subtract(int a,int b)
{
return a - b;
}
然后到工程目录下,写一个模块定义文件,新建一个TXT文件,见文件名和文件后缀修改为Dll2.def,然后将这个.def文件加入到工程中选择Project->Add To Project->Files 选择所有类型文件,然后加载到工程中去,在工程中对该文件进行编写,编写如下:
LIBRART Dll2 //Dll2与本工程生成的DLL文件的名字一致
EXPORTS //后面跟要导出的函数的名称
add
Subtract
编译链接,然后用dumpbin查看输出情况
结果显示,没有发生名字改编
即使在函数前面修改调用约定为_stdcall 函数的名字也不会发生改编的,因为在模块定义文件中已经指出了函数的名字。但是如果将DLL文件中的函数调用约定该为标准调用约定的话,那么在客户端程序中 也需要将函数的调用约定修改为标准调用约定,否则会出错。也就是说调用约定要一致。
如何动态加载动态链接库
首先将Dll1Test工程进行修改,将Project->Settings->LINK中的Dll1.lib去掉
然后将所有响应函数中的所有内容全部注释掉,然后添加如下代码进去:
void CDll1TestDlg::OnBtnAdd()
{
// TODO: Add your control notification handler code here
/*
CString str;
str.Format("5+3=%d",add(5,3));
MessageBox(str);*/
HINSTANCE hInst; //定义一个实例的句柄变量
hInst = LoadLibrary("Dll2.dll");//LoadLibrary函数映射指定的可执行模块到一个调用进程的地址空间中,不仅可以加载DLL,也可以加载可执行程序,该函数有一个参数,参数为加载的可执行模块的名字(文件名可以为.dll文件和.exe文件)
typedef int (*ADDPROC)(int a,int b); //定义一个函数指针类型
ADDPROC Add = (ADDPROC)GetProcAddress(hInst,"add");//获取导出函数的地址 GetProcAddress用来获取动态链接库导出的函数的地址 成功的话返回函数地址,否则返回NULL
if(!Add) //判断函数地址是否成功获取
{
MessageBox("获取函数地址失败!");
return;
}
CString str;
str.Format("5+3=%d",Add(5,3)); //函数指针调用函数
MessageBox(str);
}
然后将Dll2工程中编译链接生成的Dll2.dll文件拷贝到Dll1Test工程目录下,然后点击编译链接运行Dll1Test,同样可以成功实现DLL的调用。
这种调用只需要将动态链接库文件拷贝到客户端目录下,lib文件和头文件均不需要拷贝。这种调用成为动态调用,这种调用的好处是,不会将程序用到的所有的DLL全部加在到内存,只有在运行指定功能的时候,才会将用到的DLL加在到内存中,我们可以用dumpbin工具对该程序进行查看

可以看到,此时没有加载Dll2.dll
在一个程序要调用很多的DLL时,如果使用隐式链接,那将会把所有用到的DLL全部加载到内存中,那样程序启动将会很慢,所以建议使用动态调用DLL。
如果不进行名字改编约束,可用使用如下两种方法进行调用
ADDPROC Add = (ADDPROC )GetProcAddress(hInst,”?add@@YAHHH@2”); //使用改编之后的函数名直接调用
ADDPROC Add = (ADDPROC )GetProcAddress(hInst,MAKEINTRESOURCE(1));//使用导出函数序号进行调用 不建议使用
动态链接库的释放
如果在功能函数结束之后,以后都不需要加载该动态链接库的时候,可以用FreeLibrary函数来释放动态链接库
FreeLibrary(hInst);
注意:
一、动态链接库导出函数的名字改编问题 (模块定义文件)
二、动态链接库导出函数的调用约定要一致
DllMain
DllMain函数 动态链接库的入口点。是一个可选的函数。在这个函数中不要做一些复杂的加载,只需要写一些内存分配等简单的操作