注:.dll文件是Windows平台下的动态链接库文件,在Linux平台,有响应功能的文件是.so文件,.so文件接口的封装也可以参考此文的思路
目录
一、DLL项目结构介绍
二、DLL项目示例
1.文件 My_Dll_Project.h
2.文件 My_Dll_Project.cpp
3.修改后的文件 My_Dll_Project.h
三、封装方法介绍
1.再包装法
1.1 新接口头文件 My_Dll_Project_Interface.h(给用户的头文件)
1.2 文件 My_Dll_Project_Interface.cpp (给用户头文件的实现,不公开)
1.3 封装后的原头文件 My_Dll_Project.h (不公开)
1.4 再包装法面临的难题
2.抽象类法
2.1 抽象类接口头文件 My_Dll_Project_Interface_2.h(给用户的头文件)
2.2 文件 My_Dll_Project_Interface_2.cpp (给用户头文件的实现,不公开)
1.3 封装后的原头文件 My_Dll_Project.h (不公开)
3.大小欺骗法(原创,未深度检验)
3.1 原来给用户的接口头文件(不公开)
3.2 占位+删减处理后的头文件(公开给用户)
一个C++的DLL项目的基本结构是:
其中,源文件(.cpp)里写接口函数的具体实现,编译完成后一般不公开给用户,原因有很多,如,保留产权,或者为了彻底封装,甚至可能只是因为代码写得太烂不好意思公开(手动滑稽)。
头文件(.h),库文件(.lib),动态链接库文件(.dll)必须给用户,程序数据库文件(.pdb)给不给看心情,如果给了,用户就能在调试的多看几步。
头文件(.h)就是其他程序员(用户)使用你的库的接口了,也就是说,头文件告诉了用户如何使用你的库,原理参考C++教科书关于声明和定义的章节。
下面是用VS创建项目自动生成的代码,项目名:My_Dll_Project
// 下列 ifdef 块是创建使从 DLL 导出更简单的
// 宏的标准方法。此 DLL 中的所有文件都是用命令行上定义的 MYDLLPROJECT_EXPORTS
// 符号编译的。在使用此 DLL 的
// 任何项目上不应定义此符号。这样,源文件中包含此文件的任何其他项目都会将
// MYDLLPROJECT_API 函数视为是从 DLL 导入的,而此 DLL 则将用此宏定义的
// 符号视为是被导出的。
#ifdef MYDLLPROJECT_EXPORTS
#define MYDLLPROJECT_API __declspec(dllexport)
#else
#define MYDLLPROJECT_API __declspec(dllimport)
#endif
// 此类是从 dll 导出的
class MYDLLPROJECT_API CMyDllProject {
public:
CMyDllProject(void);
// TODO: 在此处添加方法。
};
extern MYDLLPROJECT_API int nMyDllProject;
MYDLLPROJECT_API int fnMyDllProject(void);
// My_Dll_Project.cpp : 定义 DLL 的导出函数。
//
#include "framework.h"
#include "My_Dll_Project.h"
// 这是导出变量的一个示例
MYDLLPROJECT_API int nMyDllProject=0;
// 这是导出函数的一个示例。
MYDLLPROJECT_API int fnMyDllProject(void)
{
return 0;
}
// 这是已导出类的构造函数。
CMyDllProject::CMyDllProject()
{
return;
}
这样的头文件给别人似乎看不出来什么问题,如果你感觉注释太多,删掉也可以,但是如果接口是下面这样,再直接给用户,就很成问题了。
#pragma once
#ifdef MYDLLPROJECT_EXPORTS
#define MYDLLPROJECT_API __declspec(dllexport)
#else
#define MYDLLPROJECT_API __declspec(dllimport)
#endif
#include
// 此类是从 dll 导出的
class MYDLLPROJECT_API CMyDllProject
{
public:
CMyDllProject(void);
void this_dll_user_is_good();
private:
int user;
char is;
void* bad;
HANDLE yeah;
private:
HANDLE user_is_bad();
void user_is_stupid();
void user_is_dirty();
void user_is_nasty();
void user_is_ugly();
void user_is_wtf_i_cant_say_more();
};
extern MYDLLPROJECT_API int nMyDllProject;
MYDLLPROJECT_API int fnMyDllProject(void);
当然这里的说用户坏话的函数名只是开个玩笑,认真地说,这个头文件有以下两个问题:
函数和变量是独立的,可以分别决定每个函数和变量是否导出,但是类的定义一定是所有成员都写在一起的,这才导致了问题,下面就来介绍常用的类的两种深度隔离的封装方法。
就是不导出原本要导出的类,重写一个新类,新类中数据成员只有一根指向原先类的指针,新类的所有普通函数都是通过指针对原先类的调用,就好像在商品外面重新包装了一样,再包装类可以完全隔绝原有类的副影响,而且可以给原先不支持移动语义(C++11)的类附加移动语义支持。
#pragma once
#ifdef MYDLLPROJECT_EXPORTS
#define MYDLLPROJECT_API __declspec(dllexport)
#else
#define MYDLLPROJECT_API __declspec(dllimport)
#endif
class CMyDllProject; // 这里写声明
class MYDLLPROJECT_API CMyDllProjectInterface
{
public:
CMyDllProjectInterface(void);
CMyDllProjectInterface(CMyDllProjectInterface&& mov) noexcept; // 添加移动语义
~CMyDllProjectInterface();
public:
CMyDllProjectInterface& operator=(CMyDllProjectInterface&& mov) noexcept; // 添加移动语义
public:
void this_dll_user_is_good();
private:
CMyDllProject* _self; // 只暴露一根指针,甚至不暴露,直接声明为void*,然后实现代码使用强制类型转换也可以
};
#include "My_Dll_Project_Interface.h"
#include "My_Dll_Project.h"
CMyDllProjectInterface::CMyDllProjectInterface(void) :
_self(new CMyDllProject)
{}
CMyDllProjectInterface::CMyDllProjectInterface(CMyDllProjectInterface&& mov) noexcept:
_self(mov._self)
{
mov._self = nullptr;
}
CMyDllProjectInterface::~CMyDllProjectInterface()
{
if (_self) delete _self;
}
CMyDllProjectInterface& CMyDllProjectInterface::operator=(CMyDllProjectInterface&& mov) noexcept
{
this->~CMyDllProjectInterface();
_self = mov._self;
mov._self = nullptr;
return *this;
}
void CMyDllProjectInterface::this_dll_user_is_good()
{
return _self->this_dll_user_is_good(); // 这个类的函数其实就是对原类的简单调用
}
#include // 这个头文件就不用再暴露给用户了
class CMyDllProject
{
public:
CMyDllProject(void);
void this_dll_user_is_good();
private:
int user;
char is;
void* bad;
HANDLE yeah;
private:
HANDLE user_is_bad();
void user_is_stupid();
void user_is_dirty();
void user_is_nasty();
void user_is_ugly();
void user_is_wtf_i_cant_say_more();
};
完美封装+添加移动语义
这种封装的优点:
缺点:
反映类的继承关系就是说下面这样:
class Interface // 这是要直接公开的抽象类接口
{
public:
virtual int help() = 0;
};
class A :public Interface // 这是要包装的导出类
{
public:
virtual int help();
void yes();
};
class B :public A // 这是要包装的导出类
{
public:
void yes();
};
这三个类想导出,封装类可不好写,读者可以自行试试如何解决这个棘手的问题,先告诉大家这个问题并不是无解。
这个方法的基本原理就是利用抽象类作接口,给用户提供工厂函数(Factory Method),或者把所有工厂函数打包在一起做个工具箱类,让用户使用工厂函数创建实例。
#ifdef MYDLLPROJECT2_EXPORTS
#define MYDLLPROJECT2_API __declspec(dllexport)
#else
#define MYDLLPROJECT2_API __declspec(dllimport)
#endif
class MYDLLPROJECT2_API CMyDllProjectInterface
{
public:
virtual void this_dll_user_is_good() = 0;
};
MYDLLPROJECT2_API CMyDllProjectInterface* factory_function(); // 导出工厂函数
#include "My_Dll_Project.h"
#include "My_Dll_Project_Interface_2.h"
CMyDllProjectInterface* factory_function()
{
return new CMyDllProject();
}
#include
#include "My_Dll_Project_Interface_2.h"
class CMyDllProject : public CMyDllProjectInterface
{
public:
CMyDllProject(void);
void this_dll_user_is_good();
private:
int user;
char is;
void* bad;
HANDLE yeah;
private:
HANDLE user_is_bad();
void user_is_stupid();
void user_is_dirty();
void user_is_nasty();
void user_is_ugly();
void user_is_wtf_i_cant_say_more();
};
这种封装的优点:
缺点:
这种方法多适用于接口先于代码确定好的情况
没有点创新怎好意思投原创,这种方法的原理非常简单,就是原来接口头文件在编译好后、交给用户前时,把类中的私有成员尽管删除,然后用char数组填充类的大小,数组长度为sizeof(类)的大小,说白了就是占位的作用。
#pragma once
#ifdef MYDLLPROJECT_EXPORTS
#define MYDLLPROJECT_API __declspec(dllexport)
#else
#define MYDLLPROJECT_API __declspec(dllimport)
#endif
#include
class MYDLLPROJECT_API CMyDllProject
{
public:
CMyDllProject(void);
void this_dll_user_is_good();
private:
int user;
char is;
void* bad;
HANDLE yeah;
private:
HANDLE user_is_bad();
void user_is_stupid();
void user_is_dirty();
void user_is_nasty();
void user_is_ugly();
void user_is_wtf_i_cant_say_more();
};
#pragma once
#ifdef MYDLLPROJECT_EXPORTS
#define MYDLLPROJECT_API __declspec(dllexport)
#else
#define MYDLLPROJECT_API __declspec(dllimport)
#endif
// 使用占位可以不包含了
class MYDLLPROJECT_API CMyDllProject
{
public:
CMyDllProject(void);
void this_dll_user_is_good();
private:
// 函数尽管删除,数据成员反应在占位数组的大小里
char _place_hoder[20]; // 占位20
};
包不包含
这种封装的好处:
缺点:
之所以想出这种方法,是基于我的一个假设:类的定义体在编译过程中只起到内存大小指示的作用,内存本身并不划分为不同类型。我认为基于这种方法并不能完全封装,我觉得它的用处可能更多在于团队内部合作用到。
另外我认为,完全封装并不是不行,只是需要编译器支持,让编译器不导出私有函数就行了。