动态链接库
Windows API中的所有函数都包含在DLL中。其中有三个最重要的DLL,Kernel32.dll,它包含用于管理内存、进程和线程的各个函数;User32.dll,它包含用于执行用户界面任务(如窗口的创建和消息的传送)的各个函数;GDI32.dll,它包含用于画图和显示文本的各个函数。
---------------------------------------------------------------------------------
静态库和动态库
静态库:函数和数据被编译进一个二进制文件(通常扩展名为.LIB)。在使用静态库的情况下,在编译链接可执行文件时,链接器从库中复制这些函数和数据并把它们和应用程序的其他模块组合起来创建最终的可执行文件(.EXE文件);
在使用动态库的时候,往往提供两个文件:一个因入库和一个DLL。因入库包含被DLL导出的函数和变量的符号名,DLL包含实际的函数和数据。在编译链接可执行文件时,只需要链接引入库,DLL中的函数代码和数据并不复制到可执行文件中,在运行时候,再去加载DLL,访问DLL中导出的函数。
---------------------------------------------------------------------------------
使用动态链接库的好处
可以采用多种编程语言来编写。
增强产品的功能。
提供二次开发的平台。
简化项目管理
可以节省磁盘空间和内存。
有助于资源的共享。
有助于实现应用程序的本地化
---------------------------------------------------------------------------------
动态链接库被多个进程访问:
---------------------------------------------------------------------------------
动态链接库的加载方式:1.隐式加载 和 2.显示加载
一、先做一个隐式加载的例子:
1)用MFC向导新建一个Win32 Dynamic-Link Library的工程项目,名字叫做Dll1,
并且新建一个Dll1的C++源文件
2)在Dll1.cpp文件中,编写完成加法与减法运算的函数add()和substract()
3)编译之后,我们可以在工程目录的Debug文件夹中发现一个Dll1.dll的文件,
这就是动态链接库文件。
现在已经有了一个动态链接库,那么怎么让其他程序访问它呢?
首先,动态链接库中的文件必须被导出后才能被其它程序访问。
我们可以用VC自带的工具Dumpbin来查看那些动态链接库的函数被导出了。
可在命令行下进入Debug所在目录,输入以下命令
dumpbin -exports dll.dll
有些时候由于某种安装原因,dumpbin被认为是无效命令,可以到VC的安装目录
../Microsoft Visual Studio/VC98/Bin/下
找到VCVARS32.bat并在命令行运行,之后就能执行dumpbin命令了。
输入dumpbin -exports Dll1.dll 就可以查看函数的导出状况了,
不过我们没有看到任何导出的函数。
怎样才能让我们自己编写的函数被导出呢?
只要在每个函数定义的前面加上_declspec(dllexport)标记就行了,如下所示:
_declspec(dllexport)
int Add(int x,int y)
{
return x+y;
}
_declspec(dllexport)
int Subtract(int x,int y)
{
return x-y;
}
加了_declspec(dllexport)以后再编译一下,就会再生一个*.lib引入库文件和一个*.exp文件,里面包含的是导出的函数或者变量的符号名。而exp文件是一个输出库文件。
这时我们再一次在命令提示符下面输入命令:
dumpbin -exports Dll1.dll
就会有下面的结果出现
然后,新建一个基于对话框的MFC AppWizard 测试工程,在对话框上添加Add与SubTract按钮,
在这两个按钮中,分别调用动态链接库中所写的两个函数。
不过为了使编译器认识动态链接库的Add,Subtract,必须在之前使用两个声明:
extern int Add(int x,int y);
extern int Subtract(int x,int y);
接下来就可以在两个按钮中调用动态链接库的函数了,代码如下:
void CDllTestDlg::OnBtnAdd()
{
CString str;
str.Format("3+5=&d",Add(3,5));
MessageBox(str);
}
void CDllTestDlg::OnBtnSubtract()
{
CString str;
str.Format("5-3=%d",Subtract(5,3));
MessageBox(str);
}
注意:在编译之前需要将前面编译好的Dll1.dll和Dll1.lib拷贝到DllTest项目目录下,
同时按下F7,在工程设置中的Link选项卡的Object/library modules中
(在project--->setting--->link--->Object/Library Modules)填写Dll1.lib,
否则虽然声明了动态链接库的两个函数,在编译的时候不会出错,
但是在链接的时候会找不到这两个函数,从而报错。
如果要查看DllTest.exe文件信息,使用命令行dumpbin -imports dlltest.exe
上面说过,在调用动态链接库的两个函数之前,应该事先用extern声明一下,
其实也可以用_declspec(dllimport)标识符声明,所以我们可以用
_declspec(dllimport) int Add(int x,int y);
_declspec(dllimport) int Subtract(int x,int y);
取代
extern int Add(int x,int y);
extern int Subtract(int x,int y);
而且用这种方法有个好处,_declspec(dllimport)会告诉编译器要引用的函数是从*.lib文件中输入的,
这可以让编译器生成效率更高的代码。
---------------------------------------------------------------------------------
二、我们在编写动态链接库的时候,往往会提供一个包含导出函数的原型声明的头文件,
可以将这个头文件提供给其他使用动态链接库的开发人员,
他们就可以通过这个头文件获得导出函数的信息以及相关的说明文档。
下面在动态链接库工程中添加一个这样的头文件,步骤如下:
1)增加一个Dll1.h的头文件
2)事实上,这个头文件是提供给客户端使用的,所以我们把声明动态链接库两个函数的任务放到这里
_declspec(dllimport) int Add(int x,int y);
_declspec(dllimport) int Subtract(int x,int y);
这样,我们只要在DllTest工程中#include这个头文件,就声明了动态链接库的两个函数。
3)可以对这个头文件改进一下,让它既可以为客户端使用,也可以为动态链接库本身服务。
首先在Dll1.h中添加一个条件编译指令,代码如下:
在Dll.cpp文件,首先定义一个宏,#define DLL_API _declspec(dllexport)
然后包含这个头文件:#include "Dll.h"
接着去掉函数定义前的_declspec(dllexport)标识符,
(为什么可以去掉这个标识符,而不影响函数的导出?大家自己想想吧。)
代码如下:
这样做是为了方便外部程序调用同时方便内部程序使用,因为动态链接库中只有导出的函数才可以被使用,没有导出的函数在外部是看不到的,是不能被访问的
4)接下来导出整个类,代码:
注意:在客户端对类成员函数的访问,仍然受制于访问权限
定义类的时候在Class 与 point之间加了DLL_API就会导出整个类。
类的实现:
接下来我们在测试工程中新建一个按钮,调用动态链接库函数,代码如下:
---------------------------------------------------------------------------------
三、我们自己编写的到这里已经可以在C++程序中正常运行了,但是如果把这个动态链接库拿给其他语言用,比如拿给C语言调用,又会出问题,问题就在于C++导出或导入动态链接库会发生名字的改编,
在C语言中导入动态链接库时,不会正确地将名字改编回来,于是会找不到函数。
那么有没有办法使编译的时候不发生名字改编呢?
只要在定义DLL_API宏的时候,加上一句extern "C" (C要大写)就行了,如下:
#define DLL_API extern "c" _declspec(dllexport)
这样编译器就不会进行名字改编,导出的函数跟我们自己写的一模一样,
一个用C语言编写的客户端程序就可以调用这个用C++编写的动态链接库。
注意:用
extern "c"不让名字发生改编有两个缺点是,
一是不能导出类中的函数,只能导出全局函数;
二是如果函数使用标准调用约定_stdcall,即使用了extern "c",函数仍会发生改编。
---------------------------------------------------------------------------------
四、上面的方法可以实现编译时不发生名字改编,但是有两个缺点,特别是使用_stdcall时,
仍然会发生名字改编。下面介绍一种模块定义文件的方式来解决这个问题:
1)接下来新建一个动态链接库文件,文件名为Dll2.cpp文件代码为:
2)新建一个模块文件Dll.def(可以先新建一个文本文档,然后将它改成Dll2.def,
注意连扩展名也要改),将其加入要项目工程当中。
3)在VC编辑器中对Dll2.def进行编辑,添加代码
LIBRARY Dll2:指定动态链接库的内部名称,这句不是必须的
EXPORTS :即使调用_stdcall约定,也不会发生改编,而只会调用这里显示的
Add与Subtract:字符串
EXPORTS 语句引入了一个由一个或多个 definitions(导出的函数或数据)组成的节。每个定义必须在单独一行上。EXPORTS 关键字可以在第一个定义所在的同一行上或在前一行上。
*.def 文件可以包含一个或多个 EXPORTS 语句。
4)下面在DllTest工程中显式地加载这个动态链接库,并且测试一下Dll2。
动态加载动态链接库要用到LoadLibrary函数;
接下来在MFC文件中改写按钮void CDllTestDlg::OnBtnAdd() 代码:
注意:1.因为调用LoadLibrary时动态加载动态链接库,所以不需要头文件和.lib文件
2.如果我们在动态链接库中使用标准调用约定_stdcall来导出函数,在模块定义文件方式下,导出的函数名字不会发生改编。但是在客户端的可执行程序中,定义函数指针类型时也要采用_stdcall调用约定:
typedef int (
_stdcall * ADDPROC)(
int a ,
int b );
3.动态调用动态链接库时,最好不要发生名字改编,不然函数名字发生改编之后我们不知道函数名字就无法根据函数的名字进行调用了。当然如果知道DLL中函数的序号,这时可以使用宏MAKEINTRESOURCE把序号转变成名字,如:
ADDPROC Add=(ADDPROC)GetProcAddress( hInst,MAKEINTRESOURCE (1));
DllMain
它是一个动态链接库的可选入口点。
The DllMain function is an optional entry point into a dynamic-link library (DLL). If the function is used, it is called by the system when processes and threads are initialized and terminated, or upon calls to the LoadLibrary and FreeLibrary functions.
DllMain is a placeholder for the library-defined function name. You must specify the actual name you use when you build your DLL. For more information, see the documentation included with your development tools.
BOOL WINAPI DllMain(
HINSTANCE hinstDLL,
DWORD fdwReason,
LPVOID lpvReserved
);
当我们的动态链接库不再使用时可以调用FreeLibrary使动态链接库使用计数减1,当使用计数为零时,系统会收回模块资源
FreeLibrary
The FreeLibrary function decrements the reference count of the loaded dynamic-link library (DLL). When the reference count reaches zero, the module is unmapped from the address space of the calling process and the handle is no longer valid.
BOOL FreeLibrary(
HMODULE hModule
);