最近想做个记录日志的C++库,方便后续使用。想着使用动态库,正好没用过,学习下。概念这里不赘述。学习过程中碰到的几点,记录下来。学习是个渐进的过程,本文也是一个逐渐完善的过程。
一、Static Library
标准Turbo 2.0中的C函数库(scanf、pringf、memcpy等)来自静态库。创建方法很简单,建立win32 application工程,选择static library,添加变量、方法和类等就可以了。使用的方法如下:
#include "../LogBuilderSL/LogBuilder.h"
#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文件照常定义即可。
<LogBuilderDL.h>
class LogBuilder{ ... };
extern "C" __declspec(dllexport) LogBuilder* CreateLogBuilder(string path);
__declspec(dllexport) void DeleteLogBuilder(LogBuilder *lpLogBuilder);
1)显示(动态)加载该DLL
<Main.cpp>
#include "../LogBuilderDL/LogBuilder.h"
typedef LogBuilder*(*CreatorByPath)(string); // 宏定义函数指针类型
int _tmain(int argc, _TCHAR* argv[])
{
HINSTANCE hDll; // DLL句柄
CreatorByPath creator; // 函数指针
hDll = LoadLibrary(L"..\\Debug\\LogBuilderDL.dll");
if (hDll != NULL)
{
creator = (CreatorByPath)GetProcAddress(hDll, "CreateLogBuilder");
if (creator != NULL)
{
LogBuilder* log = creator("log.log");
log->WriteLog("Liwuqingxin", true);
}
FreeLibrary(hDll);
}
getchar();
return 0;
}
- 首先,加载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用户可直接使用该类。但是静态成员需要额外声明为导出。如下:
<LogBuilder.cpp>
API_DECLSPEC int LogBuilder::s = 0;
API_DECLSPEC int LogBuilder::fun()
{
return 0;
}
并且在用户使用时需要加上导入lib的声明:
<Main.cpp>
...
#pragma comment(lib, "../Debug/LogBuilderDL.lib") // 使用类的静态成员时需要
...
所谓的显示(动态)加载,是通过windows API函数加载DLL,并获取需要的函数地址,这个工作由API完成。而在客户程序中直接使用类的静态成员,编译会报无法解析外部符号的错,因为编译器无法找到这些符号(未调用API),那么我们只能自己显示加载.lib文件,并在DLL中声明导出静态成员。更深入理解为,我们还可以直接将“?fun@LogBuilder@@SAHXZ”传递给GetProcAddress()
函数获取静态成员的地址,这样也能不加载.lib直接使用。
2)隐式(静态)加载DLL
<Main.cpp>
#include "../LogBuilderDL/LogBuilder.h"
#pragma comment(lib, "LogBuilderDL.lib")
extern "C" __declspec(dllimport) LogBuilder* CreateLogBuilder(std::string path);
int _tmain(int argc, _TCHAR* argv[])
{
LogBuilder *log = CreateLogBuilder("log.log");
if (log != NULL)
log->WriteLog("Liwuqingxin<span style="font-family: Arial, Helvetica, sans-serif;">", true);
getchar();
return 0;
}
- 首先,包含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);
语句了)代码如下:
#ifdef HFILENAME_EXPORTING
#define API_DECLSPEC __declspec(dllexport)
#else
#define API_DECLSPEC __declspec(dllimport)
#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:本文参考了很多优秀的博客、论坛等,感谢这些大虾们的总结。在参考的地方基本上给出了原文链接。本文在此基础上进行了实验验证并做了一些整理和总结。