打开APP
userphoto
未登录

开通VIP,畅享免费电子书等14项超值服

开通VIP
动态链接库(DLL)的创建和使用

最近想做个记录日志的C++库,方便后续使用。想着使用动态库,正好没用过,学习下。概念这里不赘述。学习过程中碰到的几点,记录下来。学习是个渐进的过程,本文也是一个逐渐完善的过程。

一、Static Library

标准Turbo 2.0中的C函数库(scanf、pringf、memcpy等)来自静态库。创建方法很简单,建立win32 application工程,选择static library,添加变量、方法和类等就可以了。使用的方法如下:

  1. #include '../LogBuilderSL/LogBuilder.h'
  2. #pragma comment(lib, '../Debug/LogBuilderSL.lib')

之后便可以像C库函数一样正常使用了。

#pragma comment(lib, '../Debug/LogBuilderSL.lib')表明,本工程与静态库(参数指定的路径下的*.lib)一起编译。

或者将该lib添加到【Project Property】-->【Linker】-->【Input】下的Additional Dependencies中,添加的方式为全路径,如:“C:\Users\ SAMSUNG-PC\ Desktop\ C S\ LogBuilderSL\ Debug\ LogBuilderSL.lib”。

再或者将*.lib放置到Library Directories下(或者在其中添加*.lib路径),在上面的【Input】中添加LogBuilderSL.lib。

二、Dynamic Link Library

对于DLL,VC支持的有三类:No-MFC DLL、MFC Regular DLL和MFC Extension DLL。

  • No-MFC DLL:导出函数为标准的C接口(extern 'C');
  • MFC Regular DLL:包含一个继承自CWinApp的类,无消息循环;
  • MFC Extension DLL:采用MFC动态链接版本创建,只用于MFC类库的应用程序。

1、No-MFC DLL

动态链接库通过导出函数对外提供的接口,有两种导出函数的方法:a、通过模块定义(.ref)文件声明;b、通过关键字__declspec(dllexport)声明导出函数。这里仅讨论第二种方式,模块定义文件的方式请自行查阅。

给出简单的DLL创建方法,头文件声明了类LogBuilder和两个导出函数,其中CreateLogBuilder()函数为C风格函数。CPP文件照常定义即可。

  1. <LogBuilderDL.h>
  2. class LogBuilder{ ... };
  3. extern 'C' __declspec(dllexport) LogBuilder* CreateLogBuilder(string path);
  4. __declspec(dllexport) void DeleteLogBuilder(LogBuilder *lpLogBuilder);

1)显示(动态)加载该DLL

  1. <Main.cpp>
  2. #include '../LogBuilderDL/LogBuilder.h'
  3. typedef LogBuilder*(*CreatorByPath)(string); // 宏定义函数指针类型
  4. int _tmain(int argc, _TCHAR* argv[])
  5. {
  6. HINSTANCE hDll; // DLL句柄
  7. CreatorByPath creator; // 函数指针
  8. hDll = LoadLibrary(L'..\\Debug\\LogBuilderDL.dll');
  9. if (hDll != NULL)
  10. {
  11. creator = (CreatorByPath)GetProcAddress(hDll, 'CreateLogBuilder');
  12. if (creator != NULL)
  13. {
  14. LogBuilder* log = creator('log.log');
  15. log->WriteLog('Liwuqingxin', true);
  16. }
  17. FreeLibrary(hDll);
  18. }
  19. getchar();
  20. return 0;
  21. }
  • 首先,加载DLL;
  • 然后,获取了CreateLogBuilder()函数的地址;
  • 最后,通过函数地址调用该函数。

这里需要注意两点。

其一,以上DLL间接导出了C++类,这里通过C风格函数封装类的获取过程,获取到类的实例后可正常使用该类,但类的静态成员(需要使用域作用符访问的成员)便无法导出。另外可直接导出C++类,第三点深入讨论。当需要使用DLL中的类型、宏定义或者变量时,需要包含该DLL的头文件(显式(动态)调用时,仅仅使用函数时并不需要)。

其二,CreateLogBuilder()为C风格函数。如果不声明为extern 'C',该函数被C编译器编译后在符号库中的名字为'CreateLogBuilder',而C++编译器则会产生名称为'?CreateLogBuilder@@YAPAVLogBuilder@@V?$basic_string@DU?$char_traits@D@std@@V?$allocator@D@2@@std@@@Z'之类的外部链接符号(不同的编译器可能生成的名字不同,但是都采用了相同的机制,生成的新名字称为“mangled name”)[参考:http://www.jianshu.com/p/5d2eeeb93590]。显示(动态)加载DLL时,GetProcAddress()函数需要通过上述真实的外部链接符号名称去获取函数地址,隐式(静态)加载没有影响。因此,导出函数应使用extern 'C'声明为C风格函数更加合适(对加载方式没有要求)。

其三,导出C++类:在class关键字与类名中间添加导出声明(这里需要使用宏代替__declspec(dllexport),因为在调用DLL时需要声明导入类,直接使用该声明则DLL用户需要另外定义.h文件)。这样,DLL用户可直接使用该类。但是静态成员需要额外声明为导出。如下:

  1. <LogBuilder.cpp>
  2. API_DECLSPEC int LogBuilder::s = 0;
  3. API_DECLSPEC int LogBuilder::fun()
  4. {
  5. return 0;
  6. }

并且在用户使用时需要加上导入lib的声明:

  1. <Main.cpp>
  2. ...
  3. #pragma comment(lib, '../Debug/LogBuilderDL.lib') // 使用类的静态成员时需要
  4. ...

所谓的显示(动态)加载,是通过windows API函数加载DLL,并获取需要的函数地址,这个工作由API完成。而在客户程序中直接使用类的静态成员,编译会报无法解析外部符号的错,因为编译器无法找到这些符号(未调用API),那么我们只能自己显示加载.lib文件,并在DLL中声明导出静态成员。更深入理解为,我们还可以直接将“?fun@LogBuilder@@SAHXZ”传递给GetProcAddress()函数获取静态成员的地址,这样也能不加载.lib直接使用。

2)隐式(静态)加载DLL

  1. <Main.cpp>
  2. #include '../LogBuilderDL/LogBuilder.h'
  3. #pragma comment(lib, 'LogBuilderDL.lib')
  4. extern 'C' __declspec(dllimport) LogBuilder* CreateLogBuilder(std::string path);
  5. int _tmain(int argc, _TCHAR* argv[])
  6. {
  7. LogBuilder *log = CreateLogBuilder('log.log');
  8. if (log != NULL)
  9. log->WriteLog('Liwuqingxin<span style='font-family: Arial, Helvetica, sans-serif;'>', true);
  10. getchar();
  11. return 0;
  12. }
  • 首先,包含DLL的头文件;
  • 然后,告诉编译器.lib文件的路径(方式和1中的静态库方法一致);
  • 再次,声明导入函数,对应于DLL导出函数;
  • 最后,可以直接像正常函数一样使用了。

需要注意几点。

其一,CreateLogBuilder()为导出函数,可以用来创建类的对象。若使用导出类(前面有提到),还可以直接实例化该类(但是不推荐,会导致DLL HELL,后面详述)。

其二,全局变量需要声明导出,否则客户程序包含头文件后使用的全局变量和DLL的中的全局变量将是两份副本!

其三,extern 'C' __declspec(dllimport) LogBuilder* CreateLogBuilder(std::string path);这句声明没有似乎也可以调用该函数[参见:http://bbs.csdn.net/topics/330169671 ]。总结一下这里查阅资料的收获:

前文中,使用__declspec(dllexport)声明导出函数,这个方法没错,但是代码的写法有些问题。明确一下:一个DLL创建后,需要提供给使用者的有三个文件:.h、.lib、.dll。DLL创建者和使用者共用.h文件,但需求不一样:创建者需要声明函数为__declspec(dllexport);使用者需要声明函数为__declspec(dllimport)。因此,出于维护性和规范性考虑,使用预编译宏和宏定义区分.h文件的包含者:DLL自身加入预编译宏***_EXPORTING。否则,假如一个DLLA调用另一个DLLB而包含其头文件时,将会使用__declspec(dllexport)而错误地将DLLB中导入的函数作为DLLA的函数导出了。(如此,Main.cpp中应该不用再加入extern 'C' __declspec(dllimport) LogBuilder* CreateLogBuilder(std::string path);语句了)代码如下:

  1. #ifdef HFILENAME_EXPORTING
  2. #define API_DECLSPEC __declspec(dllexport)
  3. #else
  4. #define API_DECLSPEC __declspec(dllimport)
  5. #endif

使用DLL时,__declspec(dllimport)声明编译时明确函数为从DLL导入的外部函数,不需要间接寻址,效率更高

3)DLL HELL

bz刚开始学习DLL相关,这里参考:DLL导出类。总结一下。[参考:http://m.blog.csdn.net/blog/guyue35/16996713]

1、DLL和客户程序是分开编译的,这会导致某些编译时确定的内容在DLL中修改无法更新到客户程序(除非你重新编译客户程序,这不现实)。以下情况会导致错误:

  • 应用程序直接访问类的公有变量,而该公有变量在新DLL中定义的位置发生了变化;
  • 应用程序调用类的一个虚函数,而新的类中,该虚函数的前面又增加了一个虚函数;
  • 新类的后面增加了成员变量,并且新类的成员函数将访问、修改这些变量;
  • 修改了新类的基类,基类的大小发生了变化;
  • 其他编译时确定的内容,如C的常量(C++新特性常量为运行时确定),宏等。

2、导出类的大小、成员的位置等的改变无法通知到客户程序,要想做一个可升级的DLL,以下三点用来使DLL远离地狱:

不直接生成类的实例。对于类的大小,当我们定义一个类的实例,或使用new语句生成一个实例时,内存的大小是在编译时决定的。要使应用程序不依赖于类的大小,只有一个办法:应用程序不生成类的实例,使用DLL中的函数来生成。把导出类的构造函数定义为私有的(privated),在导出类中提供静态(static)成员函数(如NewInstance())(静态成员函数能够用类名直接调用,而一般的成员函数要使用类对象来调用,这样只有先声明对象才能调用一般成员函数,此处要先用函数来构造类对象,故为静态的)用来生成类的实例。因为NewInstance()函数在新的DLL中会被重新编译,所以总能返回大小正确的实例内存。

不直接访问成员变量。应用程序直接访问类的成员变量时会用到该变量的偏移地址。所以避免偏移地址依赖的办法就是不要直接访问成员变量。把所有的成员变量的访问控制都定义为保护型(protected)以上的级别,并为需要访问的成员变量定义Get或Set方法。Get或Set方法在编译新DLL时会被重新编译,所以总能访问到正确的变量位置。

忘了虚函数吧,就算有也不要让应用程序直接访问它。因为类的构造函数已经是私有(privated)的了,所以应用程序也不会去继承这个类,也不会实现自己的多态。如果导出类的父类中有虚函数,或设计需要(如类工场之类的框架),一定要把这些函数声明为保护的(protected)以上的级别,并为应用程序重新设计调用该虑函数的成员函数。这一点也类似于对成员变量的处理。

事实上,建议你在发布导出类的DLL的时候,重新定义一个类的声明,这个声明可以不管原来的类里的成员变量之类的,只把接口函数列在类的声明里。

[主要参考:《VC++动态链接库(DLL)编程》 系列,作者:宋宝华,http://21cnbao.blog.51cto.com/109393/120777。

PS:本文参考了很多优秀的博客、论坛等,感谢这些大虾们的总结。在参考的地方基本上给出了原文链接。本文在此基础上进行了实验验证并做了一些整理和总结。

本站仅提供存储服务,所有内容均由用户发布,如发现有害或侵权内容,请点击举报
打开APP,阅读全文并永久保存 查看更多类似文章
猜你喜欢
类似文章
【热】打开小程序,算一算2024你的财运
使用 __declspec(dllexport) 从 DLL 导出
VC 动态链接库(DLL)编程深入浅出
VC++动态链接库编程总结一
《windows核心编程系列》十七谈谈dll
C/C++ (函数、变量和类)动态库的创建、导出和使用
lib 和 dll 的区别、生成以及使用详解
更多类似文章 >>
生活服务
热点新闻
分享 收藏 导长图 关注 下载文章
绑定账号成功
后续可登录账号畅享VIP特权!
如果VIP功能使用有故障,
可点击这里联系客服!

联系客服