前面动态链接库(五)–导出类 中有介绍两种导出类的成员的方式, 即在类声明中指定导出声明或在指定成员前指定导出声明.
这种方式的缺点就是:
实例化对象的对象空间还是在使用者的模块里,dll只提供类中的函数代码. 因此使用者需要知道整个类的实现,包括基类、类中成员对象,也就是说所有跟导出类相关的东西,使用者都要知道.
通过dumpbin可以看到,这时候的dll导出的是跟类相关的函数:如构造函数、赋值操作符、析构函数、其它函数,这些都是使用者可能会用到的函数.
即这种导出类的方式,导出的东西太多、导致使用者对类的实现依赖也很多.
例: 在某个DLL1的导出类中, 有引用其他DLL2的变量, 函数或类, 那么实际使用时也需要提供DLL2甚至更多DLL的.h 和 lib文件.
最近在工作中学习到了另一种导出类的高效实现, 结构是这样的:
导出类是一个派生类,派生自一个抽象类——都是纯虚函数.
使用者需要知道这个抽象类的结构, DLL最少只需要提供一个用于获取类对象指针的接口.
使用者跟DLL提供者共用一个抽象类的头文件,使用者依赖于DLL的东西很少,只需要知道抽象类的接口,以及获取对象指针的导出函数,对象内存空间的申请是在DLL模块中做的,释放也在DLL模块中完成,最后记得要调用释放对象的函数.
定义纯虚类CDll1如下:
//CDll1.h
#pragma once
#ifdef CDLL1_EXPORTS
#define DLL1_API __declspec(dllexport)
#else
#define DLL1_API __declspec(dllimport)
#endif
//纯虚类指出导出声明
class DLL1_API CDll1
{
public:
static CDll1* CreateInstance();
static void DestroyInstance(CDll1* pInstance);
virtual void Destroy() = 0;
virtual void DllTest() = 0;
};
源文件如下:
//CDll1.cpp
#include "CDll1.h"
#include
#include
#include "Dll1.h" //派生类
CDll1* CDll1::CreateInstance()
{
OutputDebugString(_T("\n CDll1::CreateInstance \n"));
return new Dll1; //这里new的是派生类的实例
}
void CDll1::DestroyInstance(CDll1* pInstance)
{
OutputDebugString(_T("\n CDll1::DestroyInstance \n"));
pInstance->Destroy();
}
派生类Dll1实现如下:
//Dll1.h
#pragma once
#include "CDll1.h"
//派生类无需指定导出声明
class Dll1 : public CDll1
{
public:
Dll1();
~Dll1();
virtual void Destroy();
virtual void DllTest();
};
源文件如下:
//Dll1.cpp
#include "Dll1.h"
#include
#include
Dll1::Dll1()
{
OutputDebugString(_T("\n Dll1::Dll1 \n"));
}
Dll1::~Dll1()
{
OutputDebugString(_T("\n Dll1::~Dll1 \n"));
}
void Dll1::Destroy()
{
OutputDebugString(_T("\n Dll1::Destroy \n"));
if (this != NULL)
{
delete this;
}
}
void Dll1::DllTest()
{
OutputDebugString(_T("\n Dll1::Dll1Test \n"));
}
关于纯虚类中的CDLL1_EXPORTS宏, 在项目属性的预处理器中指定:
编译生成, 使用dumpbin命令查看Dll:
有一些编译默认生成的构造析构, 以及一张虚表, 后续调用均通过虚表找到实际派生类函数, 这里并未实际导出声明的纯虚函数.
将上面生成的DLL更新到使用项目中, 通过静态的CreateInstance接口创建对象, 调用成员函数, 最后别忘了通过全局的DestroyInstance接口销毁动态创建的对象.
#include
#pragma comment(lib, "./Dll1.lib")
#include "CDll1.h"
int main()
{
CDll1* pInstance = CDll1::CreateInstance();
pInstance->DllTest();
CDll1::DestroyInstance(pInstance);
getchar();
}
编译运行可以看到内存有被释放:
这里可以知道, 即使派生类没有指定导出声明, 也能够通过父类指针调用, 这得益于父类中导出的虚表.
因为父类中的成员函数都是纯虚函数, 且父类指针指向子类对象, 因此实际调用时会通过虚表找到子类的实现:
在使用导出的纯虚类时, 实际调用借助了继承体系中的多态机制, 使用父类指针指向子类对象, 因此会调用子类的实现, 即使子类未导出, 因为这里的调用发生在DLL模块中 而不在 使用项目的模块中.
这种方式比较好,通用,产生的DLL没有特定环境限制. 借助了C++类的虚函数, 一般都是采用这种方式.
除了对DLL导出类有好处外,采用接口跟实现分离,可以使得工程的结构更清晰,使用者只需要知道接口,而不需要知道实现.