• 动态链接库--导出类(二)


    写在前面

    前面动态链接库(五)–导出类 中有介绍两种导出类的成员的方式, 即在类声明中指定导出声明或在指定成员前指定导出声明.

    这种方式的缺点就是:

    实例化对象的对象空间还是在使用者的模块里,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;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    源文件如下:

    //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();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    派生类Dll1实现如下:

    //Dll1.h
    #pragma once
    
    #include "CDll1.h"
    
    //派生类无需指定导出声明
    class Dll1 : public CDll1
    {
    public:
    	Dll1();
    	~Dll1();
    	virtual void Destroy();
    
    	virtual void DllTest();
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    源文件如下:

    //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"));
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    关于纯虚类中的CDLL1_EXPORTS宏, 在项目属性的预处理器中指定:
    1

    编译生成, 使用dumpbin命令查看Dll:
    2

    有一些编译默认生成的构造析构, 以及一张虚表, 后续调用均通过虚表找到实际派生类函数, 这里并未实际导出声明的纯虚函数.

    使用导出的纯虚类

    将上面生成的DLL更新到使用项目中, 通过静态的CreateInstance接口创建对象, 调用成员函数, 最后别忘了通过全局的DestroyInstance接口销毁动态创建的对象.

    #include 
    #pragma comment(lib, "./Dll1.lib")
    #include "CDll1.h"
    
    int main()
    {
        CDll1* pInstance = CDll1::CreateInstance();
        pInstance->DllTest();
        CDll1::DestroyInstance(pInstance);
        getchar();
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    编译运行可以看到内存有被释放:
    3
    这里可以知道, 即使派生类没有指定导出声明, 也能够通过父类指针调用, 这得益于父类中导出的虚表.

    因为父类中的成员函数都是纯虚函数, 且父类指针指向子类对象, 因此实际调用时会通过虚表找到子类的实现:
    4

    总结

    在使用导出的纯虚类时, 实际调用借助了继承体系中的多态机制, 使用父类指针指向子类对象, 因此会调用子类的实现, 即使子类未导出, 因为这里的调用发生在DLL模块中 而不在 使用项目的模块中.

    这种方式比较好,通用,产生的DLL没有特定环境限制. 借助了C++类的虚函数, 一般都是采用这种方式.

    除了对DLL导出类有好处外,采用接口跟实现分离,可以使得工程的结构更清晰,使用者只需要知道接口,而不需要知道实现.

  • 相关阅读:
    众佰诚:抖音开网店新手怎么做才能做起来
    a元素的几种伪类选择器
    [ZOOKEEPER]zookeeper基础知识笔记
    【Proteus仿真】【Arduino单片机】HC05蓝牙通信
    第五章 变形
    MessageQueue消息队列——基础(笔记)
    ArrayList扩容机制分析
    tomcat启动后,执行一个方法作为监听
    【OPENVX】对象基本使用之vx_pyramid
    流行的Python库numpy及Pandas简要介绍
  • 原文地址:https://blog.csdn.net/SNAKEpc12138/article/details/126554161