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

最近想做个记录日志的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:本文参考了很多优秀的博客、论坛等,感谢这些大虾们的总结。在参考的地方基本上给出了原文链接。本文在此基础上进行了实验验证并做了一些整理和总结。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值