关于MFC的序列化与反序列化机制
2023-4-4 08:33:34 Author: 0x00实验室(查看原文) 阅读量:16 收藏

概述

序列化操作:将类对象的数据部分按照一定的规则进行二进制摆放,便于传输和存储等操作。

反序列化:将二进制数据按照一定的规则填充到指定的类对象的数据区中,完成对象的数据填充操作。

MFC的序列化对象的构建需要满足以下要求:

  • 可序列化对象必须是CObject的子类。

  • 在类内部声明 DECLARE_SERIAL 宏,并且类中必须存在无参构造。

  • 将 Serialize(CArchive & ar) 函数进行重构,编写适应当前类对象的序列化处理操作。

  • 在类外部声明 IMPLEMENT_SERIAL 宏,确定可序列化宏和序列化操作的版本信息。

MFC程序中部分进行序列化操作的代码,如下所示:

class CMyLine :public CObject
{
public:
   //声明此类型是序列化类型
DECLARE_SERIAL(CMyLine)
       
CPoint m_ptFrom;
CPoint m_ptTo;

public:
CMyLine()
{
}
CMyLine(CPoint from, CPoint to)
{
m_ptFrom = from;
m_ptTo = to;
}

virtual void Serialize(CArchive& ar);
};
//声明序列化对象版本
IMPLEMENT_SERIAL(CMyLine, CObject, 1);

void CMyLine::Serialize(CArchive& ar)
{
if (ar.IsStoring())
{
ar << m_ptFrom << m_ptTo;
} else
{
ar >> m_ptFrom >> m_ptTo;
}
}

void CMFC8Dlg::OnBnClickedButton1()
{
   //打开一个文件
CFile file(TEXT("1.txt"), CFile::modeCreate | CFile::modeWrite);

   //创建序列化操作对象,该对象与文件对象关联
CArchive ar(&file, CArchive::store);

//创建序列化对象
CMyLine line(CPoint(0, 0), CPoint(1, 2));

   //对象进行序列化操作
line.Serialize(ar);

   //关闭序列化操作对象
ar.Close();
   //关闭文件对象
file.Close();
}

上述代码中有一对代码段比较有意思:

//声明当前类是一个可序列化类DECLARE_SERIAL(CMyLine)
//声明序列化对象版本IMPLEMENT_SERIAL(CMyLine, CObject, 1);

这对代码和MFC的消息映射机制类似,代码如下:

//声明当前函数存在消息映射DECLARE_MESSAGE_MAP()
BEGIN_MESSAGE_MAP(CMFC2Dlg, CDialogEx)//声明映射哪个类对象/*消息映射*/ //声明映射绘图消息END_MESSAGE_MAP()//结束消息映射

序列化宏分析

//在类内部声明的DECLARE_SERIAL宏#define DECLARE_SERIAL(class_name) \_DECLARE_DYNCREATE(class_name) \AFX_API friend CArchive& AFXAPI operator>>(CArchive& ar, class_name* &pOb);

将宏展开,得到下述代码,其中涉及到

//进行宏展开protected:static CRuntimeClass* PASCAL _GetBaseClass();//官方文档未声明public:static CRuntimeClass class##class_name;//根据指定的类名,定义CRuntimeClass类型成员
static CRuntimeClass* PASCAL GetThisClass();//官方文档未声明(可能用于初始化CRuntimeClass成员)virtual CRuntimeClass* GetRuntimeClass() const;//返回与此对象的类对应的 CRuntimeClass 结构。static CObject* PASCAL CreateObject();//返回当前类对象,函数声明在另一个宏中AFX_API friend CArchive& AFXAPI operator>>(CArchive& ar, class_name* &pOb);//右移运算的友元函数
struct CRuntimeClass
{
// 属性部分
LPCSTR m_lpszClassName; //类的名称。
int m_nObjectSize; //对象大小(以字节为单位)。
UINT m_wSchema; //类的架构编号。(-1 表示不可序列化的类)
CObject* (PASCAL* m_pfnCreateObject)(); //指向动态创建对象的函数的指针。
  //指向创建类对象的默认构造函数的函数指针
#ifdef _AFXDLL //DLL动态链接时
CRuntimeClass* (PASCAL* m_pfnGetBaseClass)(); //指向返回基类 CRuntimeClass 结构的函数地址
#else //静态链接时
CRuntimeClass* m_pBaseClass; //指向基类的 CRuntimeClass 结构的指针。
#endif
   
// 函数部分
   //在运行时创建对象。
CObject* CreateObject();
   //确定该类是否派生自指定的类。
BOOL IsDerivedFrom(const CRuntimeClass* pBaseClass) const;

//动态名称查找和创建
static CRuntimeClass* PASCAL FromName(LPCSTR lpszClassName);
static CRuntimeClass* PASCAL FromName(LPCWSTR lpszClassName);
static CObject* PASCAL CreateObject(LPCSTR lpszClassName);
static CObject* PASCAL CreateObject(LPCWSTR lpszClassName);

// Implementation
void Store(CArchive& ar) const;
static CRuntimeClass* PASCAL Load(CArchive& ar, UINT* pwSchemaNum);

// CRuntimeClass objects linked together in simple list
CRuntimeClass* m_pNextClass;       // linked list of registered classes
const AFX_CLASSINIT* m_pClassInit;
};

经过上述代码的分析,大致可以观测出,一切操作都是围绕着当前类对象的CRuntimeClass类型成员展开的。

继续查看序列化的另一个宏定义内容:

#define IMPLEMENT_SERIAL(class_name, base_class_name, wSchema) \  CObject* PASCAL class_name::CreateObject() \    { return new class_name; } \  extern AFX_CLASSINIT _init_##class_name; \  _IMPLEMENT_RUNTIMECLASS(class_name, base_class_name, wSchema, \    class_name::CreateObject, &_init_##class_name) \  AFX_CLASSINIT _init_##class_name(RUNTIME_CLASS(class_name)); \  CArchive& AFXAPI operator>>(CArchive& ar, class_name* &pOb) \    { pOb = (class_name*) ar.ReadObject(RUNTIME_CLASS(class_name)); \      return ar; }

将 IMPLEMENT_SERIAL 宏整理展开:

//返回当前类型的指针
CObject* PASCAL class_name::CreateObject()
{
return new class_name;
}

/*
struct AFX_CLASSINIT
{ AFX_CLASSINIT(CRuntimeClass* pNewClass) { AfxClassInit(pNewClass); } };
*/
//声明操作,类具体定义在下方
extern AFX_CLASSINIT _init_##class_name;

CRuntimeClass* PASCAL class_name::_GetBaseClass()
{
return RUNTIME_CLASS(base_class_name);
}

//在全局初始化类中的成员变量
AFX_COMDAT CRuntimeClass class_name::class##class_name = {
#class_name, //声明类名
sizeof(class class_name), //计算类大小
wSchema, //当前序列化的版本信息
class_name::CreateObject, //对象创建的函数地址
&class_name::_GetBaseClass, //_GetBaseClass函数地址
NULL,
&_init_##class_name //初始化类对象的地址
};

//获取 CRuntimeClass 对象地址
CRuntimeClass* PASCAL class_name::GetThisClass()
{
return ((CRuntimeClass*)(&class_name::class##class_name));
}

//获取 CRuntimeClass 对象地址
CRuntimeClass* class_name::GetRuntimeClass() const
{
return ((CRuntimeClass*)(&class_name::class##class_name));
}

//初始化类定义
AFX_CLASSINIT _init_##class_name(class_name::GetThisClass());

//将友元函数的实现部分
CArchive& AFXAPI operator>>(CArchive& ar, class_name* &pOb)
{
//类对象的右移运算符重载,用于数据的加载操作
pOb = (class_name*) ar.ReadObject(class_name::GetThisClass());
return ar;
}

综上可以看见 CRuntimeClass 结构在序列化对象中的重要性。其中记录了类对象的信息,以及基类对象的 CRuntimeClass 地址或者函数地址。

在VS观察,如下图所示:

细说序列化对象的构建要求

可序列化对象必须是CObject的子类。

CObject类中存在一个需要重构的 CObject::Serialize 函数,该函数是进行序列化操作的具体细节实现。

在类内部声明 DECLARE_SERIAL 宏,并且类中必须存在无参构造。

DECLARE_SERIAL 宏主要是对CRuntimeClass 结构操作的相关函数声明,如果不进行声明,那么类外部声明IMPLEMENT_SERIAL宏时,就会报错。至于类对象中必须存在一个无参构造,主要是因为 class_name::CreateObject 函数会调用并创建一个类对象,创建时调用的就是无参构造。

将 Serialize(CArchive & ar) 函数进行重构,编写适应当前类对象的序列化处理操作。

此条件就不需要进行细说,想要将本类中哪些数据成员进行序列化成二进制进行存储,具体规则都需要在该函数中实现。

在类外部声明 IMPLEMENT_SERIAL 宏,确定可序列化宏和序列化操作的版本信息。

IMPLEMENT_SERIAL 是DECLARE_SERIAL 宏的实现部分,并且该宏声明了当前序列化的版本。至于什么是序列化版本在接下来将会讲述。

序列化版本问题

最开始的类对象。此时只需要序列化两个成员数据。

class A{public:  int a;    int b;}

而后来根据业务需求,需要将类进行迭代升级。那么就有两种处理方式:

  • 处理方法一:新类继承A类,并重写Serialize 函数

  • 处理方法二:判断序列化的版本信息,根据版本进行选择性的序列化、反序列化操作

//处理方法一:略
//处理方法二

//提示版本更新
IMPLEMENT_SERIAL(CMyLine, CObject, 2|VERSIONABLE_SCHEMA);

//序列化操作函数
void CMyLine::Serialize(CArchive& ar)
{
if (ar.IsStoring())
{
//还是按照版本一进行序列化
ar << m_ptFrom << m_ptTo;
} else
{
//获取当前的序列化操作的版本
UINT uSchema = ar.GetObjectSchema();

//按照版本二进行反序列化
switch (uSchema)
{
case 1:{
//版本1
break;
}
case 2:{
//版本2
break;
}
}
}
}

那么在上述情况下,对不同的序列化文件进行反序列化操作时,需要进行如下步骤:

  • 获取当前文件的序列化版本(重点)

  • 进入不同的反序列化处理操作模块

获取序列化版本问题

既然我们能够重写序列化函数,那么我们就可以直接通过对象对象调用:

CFile file(TEXT("1.txt"), CFile::modeCreate | CFile::modeWrite);
//创建序列化操作对象,该对象与文件对象关联CArchive ar(&file, CArchive::store);
//创建序列化对象CMyLine line(CPoint(1, 2), CPoint(3, 4));
//对象进行序列化操作line.Serialize(ar);

新问题也是由此产生,如果我们直接调用序列化操作函数,那么函数中序列化时,序列化版本怎么获取?怎么存储?也就是说,序列化时仅仅将成员数据转换成了二进制,并保存到了数据文件中,但是并没有保存序列化的版本信息。

因此在进行反序列化操作时,ar.GetObjectSchema();函数只要执行就会报错。如下图所示:

可以明显的看到,存储的结果中只有成员信息,没有版本等其他数据信息。

解决办法

  • 方法一:直接使用CArchive对象的“>>”、“<<”友元函数进行操作。

    CFile file(TEXT("1.txt"), CFile::modeCreate | CFile::modeWrite);CArchive ar(&file, CArchive::store);CMyLine line(CPoint(00), CPoint(12));//存储版本信息,和成员的数据信息ar << &line;ar.Close();file.Close();

  • 方法二:在序列化操作时,将上述信息手动进行录入。

总结

  • 可以看出微软很喜欢这种成对的宏定义操作(序列化、消息映射)

  • 审计这种代码的确能帮助开拓视野,感兴趣的兄弟可以自己试着分析分析


文章来源: http://mp.weixin.qq.com/s?__biz=Mzg5MDY2MTUyMA==&mid=2247489150&idx=1&sn=e075e192a7e281090db59cc00ae5e58b&chksm=cfd86981f8afe0977dd18428152c8de717111f7ff4255098c008c536aada0cc071cd3a972521#rd
如有侵权请联系:admin#unsafe.sh