序列化操作:将类对象的数据部分按照一定的规则进行二进制摆放,便于传输和存储等操作。
反序列化:将二进制数据按照一定的规则填充到指定的类对象的数据区中,完成对象的数据填充操作。
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(0, 0), CPoint(1, 2));
//存储版本信息,和成员的数据信息
ar << &line;
ar.Close();
file.Close();
方法二:在序列化操作时,将上述信息手动进行录入。
可以看出微软很喜欢这种成对的宏定义操作(序列化、消息映射)
审计这种代码的确能帮助开拓视野,感兴趣的兄弟可以自己试着分析分析