动态链接库
1. 动态链接库的分类
Visual C++支持三种DLL,它们分别是Non-MFC DLL(非MFC动态库)、 MFC Regular DLL(MFC规则DLL)、 MFC Extension DLL(MFC扩展DLL)。
(1) 非MFC动态库:不采用MFC类库结构,其导出函数为标准的C接口,能被非MFC或MFC编写的应用程序所调用。
(2) MFC规则DLL:包含一个继承自CWinApp的类,但其无消息循环。
(3) MFC扩展DLL:采用MFC的动态链接版本创建,它只能被用MFC类库所编写的应用程序所调用。
2. DLL中导出函数的声明方式
一种方式是:在函数声明中加上__declspec(dllexport)。
另外一种方式是:采用模块定义(.def)文件声明,(.def)文件为链接器提供了有关被链接程序的导出、属性及其他方面的信息。
(1) 方式一:在函数声明中加上__declspec(dllexport)
1 #pragma once 2 3 extern "C" __declspec(dllexport) int add(int x, int y);
1 #include "stdafx.h" 2 #include "dllTest.h" 3 4 int add(int x, int y) 5 { 6 return (x + y); 7 }
(2) 方式二:采用模块定义(.def)文件声明
①首先创建 一个DLL程序(DllTestDef)
②在*.cpp中添加需要导出的接口函数
1 #include "stdafx.h" 2 3 int __stdcall Add(int a, int b) 4 { 5 return (a + b); 6 } 7 8 int _stdcall Sub(int a, int b) 9 { 10 return (a - b); 11 }
③然后创建一个.def的文件
1 LIBRARY dllTest 2 EXPORTS 3 Add @ 1 4 Sub @ 2
3. DLL的调用方式
(1)动态调用:
"LoadLibrary-GetProcAddress-FreeLibrary"系统API提供的三位一体"DLL加载-DLL函数地址获取-DLL释放"方式,这种调用方式称为DLL的动态调用。
1 #include "stdafx.h" 2 #include <iostream> 3 using namespace std; 4 #include <windows.h> 5 6 typedef int(*lpAdd)(int, int); 7 typedef int(*lpSub)(int, int); 8 9 int main() 10 { 11 HMODULE hDll = NULL; 12 lpAdd pAdd = NULL; 13 lpSub pSub = NULL; 14 15 hDll = LoadLibrary(TEXT("dllTest.dll")); 16 if (hDll != NULL) 17 { 18 pAdd = (lpAdd)GetProcAddress(hDll, "Add"); 19 pSub = (lpSub)GetProcAddress(hDll, "Sub"); 20 21 if (pAdd != NULL) 22 { 23 cout << "3 + 5 = " << pAdd(3, 5) << endl; 24 } 25 26 if (pSub != NULL) 27 { 28 cout << "2 + 4 = " << pSub(2, 4) << endl; 29 } 30 } 31 32 getchar(); 33 return 0; 34 }
(2)静态调用:
静态调用,也称为隐式调用,由编译系统完成对DLL的加载和应用程序结束时DLL卸载的编码(Windows系统负责对DLL调用次数的计数),调用方式简单,能够满足通常的要求。通常采用的调用方式是把产生动态连接库时产生的.LIB文件加入到应用程序的工程中,想使用DLL中的函数时,只须在源文件中声明一下。
1 #include "stdafx.h" 2 #include <iostream> 3 using namespace std; 4 5 #pragma comment(lib, "..\\x64\\Release\\dllTest.lib") 6 extern "C" __declspec(dllimport) int add(int x, int y); 7 8 int main() 9 { 10 cout << "4 + 7 = " << add(4, 7) << endl; 11 12 getchar(); 13 return 0; 14 }
4. DllMain函数
Windows在加载DLL的时候,需要一个入口函数,如同控制台或 DOS 程序需要 main 函数、WIN32程序需要 WinMain 函数一样。
在前面的例子中,DLL并没有提供 DllMain函数, 应用工程也能成功引用 DLL,这是因为Windows在找不到DllMain的时候, 系统会从其它运行库中引入一个不做任何操作的缺省 DllMain函数版本,并不意味着 DLL可以放弃 DllMain函数。
根据编写规范, Windows必须查找并执行 DLL里的 DllMain函数作为加载 DLL的依据,它使得 DLL得以保留在内存里。这个函数并不属于导出函数,而是 DLL 的内部函数。这意味着不能直接在应用工程中引用 DllMain函数, DllMain是自动被调用的 。
1 BOOL APIENTRY DllMain( HMODULE hModule, 2 DWORD ul_reason_for_call, 3 LPVOID lpReserved 4 ) 5 { 6 switch (ul_reason_for_call) 7 { 8 case DLL_PROCESS_ATTACH: 9 case DLL_THREAD_ATTACH: 10 case DLL_THREAD_DETACH: 11 case DLL_PROCESS_DETACH: 12 break; 13 } 14 return TRUE; 15 }
5. 关于调用约定
C/C++缺省的调用方式是__cdecl方式,Windows API使用__stdcall调用方式,在DLL导出函数中,为了跟Windows API保持一致,建议使用__stdcall调用方式。
__cdecl方式与__stdcall对函数名最终生成符号的方式不同。若采用C编译方式(在C++中需将函数声明为extern "C")。
__stdcall调用约定在输出函数名钱加下划线,后面加“@”符号和参数的字节数,形如_functionname@number;
而__cdecl调用约定仅在输出函数名前加下划线,形如_functionname。
6. DLL导出变量
DLL定义的全局变量可以被调用进程访问,DLL也可以访问调用进程的全局数据。
(1) 在DLL中导出变量有两种方法:
方法一:用模块定义文件(.def)进行导出声明
1 // dllmain.cpp : 定义 DLL 应用程序的入口点。 2 #include "stdafx.h" 3 4 int dllGlobalVar; 5 6 BOOL APIENTRY DllMain( HMODULE hModule, 7 DWORD ul_reason_for_call, 8 LPVOID lpReserved 9 ) 10 { 11 switch (ul_reason_for_call) 12 { 13 case DLL_PROCESS_ATTACH: 14 dllGlobalVar = 123; 15 break; 16 case DLL_THREAD_ATTACH: 17 case DLL_THREAD_DETACH: 18 case DLL_PROCESS_DETACH: 19 break; 20 } 21 return TRUE; 22 } 23 24 25 26 LIBRARY dllExportVariable_def 27 EXPORTS 28 dllGlobalVar DATA
特别要注意的是用extern int dllGlobalVar声明所导入的并不是DLL中全局变量本身,而是其地址,应用程序必须通过强制指针转换来使用DLL中的全局变量。这一点,从*(int*)dllGlobalVar可以看出。因此在采用这种方式引用DLL全局变量时,千万不要进行这样的赋值操作:
dllGlobalVar = 1;
其结果是dllGlobalVar指针的内容发生变化,程序中以后再也引用不到DLL中的全局变量了。
而通过_declspec(dllimport)方式导入的就是DLL中全局变量本身而不再是其地址了,笔者建议在一切可能的情况下都使用这种方式。
方法二:用__declspec进行导出声明
1 __declspec(dllexport) extern int dllGlobalVar = 88;
(2) 调用DLL中导出的变量:
同样,应用程序调用DLL中的变量也有两种方法。
第一种是隐式链接:
1 #include <iostream> 2 using namespace std; 3 4 #pragma comment(lib, "..\\x64\\Debug\\dllExportVariable_declspec.lib") 5 extern _declspec(dllimport) int dllGlobalVar; 6 7 int main() 8 { 9 cout << "dllGlobalVar = " << dllGlobalVar << endl; 10 11 dllGlobalVar = 88; 12 cout << "dllGlobalVar = " << dllGlobalVar << endl; 13 14 getchar(); 15 return 0; 16 }
第二种是显式链接:
1 #include <iostream> 2 using namespace std; 3 4 #include <windows.h> 5 6 int main() 7 { 8 int my_int; 9 HINSTANCE hInstLibrary = LoadLibrary(TEXT("dllExportVariable_def.dll")); 10 11 if (hInstLibrary != NULL) 12 { 13 my_int = *(int*)GetProcAddress(hInstLibrary, "dllGlobalVar"); 14 cout << "my_int = " << my_int << endl; 15 } 16 FreeLibrary(hInstLibrary); 17 18 getchar(); 19 return 0; 20 }
Note:一般不建议从DLL中导出全局变量,对于希望从DLL获取资源以实现资源共享的情景,最好是通过导出一个Get函数获得,这样操作起来更方便而且更安全。
7. DLL导出类
一、导出类的简单方式
这种方式是比较简单的,同时也是不建议采用的不合适方式。
只需要在导出类加上__declspec(dllexport),就可以实现导出类。对象空间还是在使用者的模块里,dll只提供类中的函数代码。
不足的地方是:使用者需要知道整个类的实现,包括基类、类中成员对象,也就是说所有跟导出类相关的东西,使用者都要知道。通过Dependency Walker可以看到,这时候的dll导出的是跟类相关的函数:如构造函数、赋值操作符、析构函数、其它函数,这些都是使用者可能会用到的函数。
这种导出类的方式,除了导出的东西太多、使用者对类的实现依赖太多之外,还有其它问题:必须保证使用同一种编译器。导出类的本质是导出类里的函数,因为语法上直接导出了类,没有对函数的调用方式、重命名进行设置,导致了产生的dll并不通用。
简单方式导出类的DLL示例:
1 #pragma once 2 3 //相关的类都必须导出 4 class _declspec(dllexport) CBase 5 { 6 public: 7 void Test1(); 8 private: 9 int m_var1; 10 }; 11 12 //相关的类都必须导出 13 class _declspec(dllexport) CData 14 { 15 public: 16 void Test2(); 17 private: 18 int m_var2; 19 }; 20 21 //要导出的类 22 class _declspec(dllexport) CExportClass : public CBase 23 { 24 public: 25 CExportClass(int i = 0); 26 27 void TestFun(); 28 CData GetDataObj() { return m_DataObj; } 29 30 private: 31 int m_i; 32 CData m_DataObj; 33 };
1 CExportClass::CExportClass(int i) : m_i(i) 2 { 3 } 4 5 void CExportClass::TestFun() 6 { 7 cout << "This is TestFun() from CExportClass class!" << endl; 8 } 9 10 void CBase::Test1() 11 { 12 cout << "This is Test1() from CBase class!" << endl; 13 } 14 15 void CData::Test2() 16 { 17 cout << "This is Test2() from CData class!" << endl; 18 }
调用示例:
1 #include "stdafx.h" 2 #include <Windows.h> 3 #include "..\dllTest_ExportClass(NotRecommend)\dllTest_ExportClass(NotRecommend).h" 4 5 #pragma comment(lib, "..\\x64\\Debug\\dllTest_ExportClass(NotRecommend).lib") 6 7 int main() 8 { 9 CExportClass obj(55); 10 obj.Test1(); 11 obj.TestFun(); 12 13 CData DataObj = obj.GetDataObj(); 14 DataObj.Test2(); 15 16 system("pause"); 17 return 0; 18 }
二、导出类的较好方式
这种方式和COM类似,它的结构是这样的:导出类是一个派生类,派生自一个抽象类(都是纯虚函数)。使用者只需知道这个抽象类的结构。
DLL最少需要提供一个用于获取类对象指针的接口。使用者和DLL提供者共用一个抽象类的头文件。使用者依赖于DLL的东西很少,只需要知道抽象类的接口,以及获取对象指针的导出函数,对象内存空间的申请是在DLL模块中做的,释放也在DLL模块中完成(需要在最后调用释放对象的函数)。
这种方式比较好,通用,产生的DLL没有特定的环境限制。除了对DLL导出类有好处外,它采用接口和实现分离,也可以使得工程结构更清晰,使用者只需要知道接口,不需要知道实现。
测试示例代码下载地址: https://files.cnblogs.com/files/YQ2014/dllTest_ExportClass%28NotRecommend%29.zip
参考资料:
http://www.cnblogs.com/cswuyg/archive/2011/10/06/DLL2.html
http://www.codeproject.com/KB/cpp/howto_export_cpp_classes.aspx
导出类的DLL要小心DLL Hell问题。
详细的可以参考:DLL导出类避免地狱问题的完美解决方案
posted on 2018-10-17 14:59 FlyingPig007 阅读(467) 评论(0) 编辑 收藏 举报