65.9K
CodeProject 正在变化。 阅读更多。
Home

MFC 序列化的全部内容

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.99/5 (47投票s)

2017年3月16日

MIT

19分钟阅读

viewsIcon

70388

downloadIcon

1588

描述了 MFC 序列化机制的所有方面

目录

 

引言

数据结构的世界是广阔的。当我们需将这些巨大的数据块写入磁盘、内存或套接字,或者从中读取时,MFC 序列化是每个程序员工具箱中的强大工具。

背景

序列化自 MFC(微软基础类库)首次推出以来就是其一部分,但我感觉它从未得到应有的重视,因为它在很大程度上是未被充分文档化的。演示序列化的 SDK 示例非常有限,仅涵盖了普通旧数据和 CObject 派生类及集合的序列化。然而,通过适当的扩展,我们可以序列化任何存在的数据结构、STL 集合、用户定义的集合、任何集合(包括扁平 C 风格数组)。毫无疑问,这是将分层数据存储到磁盘、内存或套接字以及从中检索数据的最强大、最高效、最快捷的方式。MFC 序列化支持对磁盘、内存和套接字的读写。写入内存对于进程间通信(如剪贴板剪切/复制/粘贴操作)非常有用,而写入套接字则在与远程机器进行网络通信时非常有用。本文将介绍使用 MFC 提供的类的普通 MFC 序列化,如何序列化 STL 集合,如何序列化普通的 Windows SDK 数据结构,如何序列化 C 风格数组,如何序列化到进程和共享内存,以及如何序列化到套接字和从套接字序列化。此外,我还将演示如何在有或没有文档/视图架构的情况下(例如在控制台应用程序和 TCP/IP 服务器中)使用 MFC 序列化。

什么是序列化

MSDN 文档给出了最好的描述

序列化是将对象转换为字节流以便存储对象或将其传输到内存、数据库或文件的过程。其主要目的是保存对象的状态,以便在需要时能够重新创建它。相反的过程称为反序列化。

MFC 序列化实现了二进制和文本序列化。二进制通过移位运算符(<<、>>)和 WriteObject / ReadObject 函数处理。文本序列化通过 ReadString / WriteString 函数处理。

MFC 序列化提供了对 C++ CObject 派生类的带版本控制的序列化。通过适当的扩展,它也可以为非 CObject 派生类提供序列化。然而,在这些情况下,版本控制需要手动处理。

它是如何工作的

MFC 序列化的核心是 CArchive 对象。CArchive 没有基类,它与 CFileCFile 的派生类(如 CSocketFileCSharedFileCMemFile)紧密耦合工作。CArchive 内部封装了一个字节数组,该数组根据需要动态增长,并写入或读取自 CFileCFile 派生对象。

  • CFile – 提供到磁盘或从磁盘的序列化
  • CMemFile – 提供到进程内存或从进程内存的序列化
  • CSharedFile – 提供到或从其他进程可访问的进程共享内存的序列化
  • CSocketFile – 为网络通信提供到 CSocket 或从 CSocket 的序列化
  • 你还可以通过命名管道、RPC 和其他 Windows 进程间通信机制进行序列化

CArchive 提供了普通旧数据和带版本控制的 C++ CObject 派生类的序列化。要使一个 CObject 类可序列化,你只需要添加一个宏

// In the class declaration
DECLARE_SERIAL(CRoot)
 
// In the class implementation
IMPLEMENT_SERIAL(CRoot, CObject, VERSIONABLE_SCHEMA | 1)

这两个宏会向你的类中添加全局提取运算符 >>(它调用 CArchive::ReadObject)、静态函数 CreateObjectCRuntimeClass 成员变量。CRuntimeClass 结构体有一个 m_lpszClassName 成员,它存储了你类名的文本表示。CRuntimeClass 还有一个 m_wSchema,它保存了你类的版本信息。

这些宏在内部展开为以下代码

//
// DECLARE_SERIAL(CRoot) expands to
//
public:
         static CRuntimeClass classCRoot;
         virtual CRuntimeClass* GetRuntimeClass() const;
         static CObject* PASCAL CreateObject();
         AFX_API friend CArchive& AFXAPI operator >> (CArchive& ar, CRoot* &pOb);
 
 
 
//
// IMLEMENT_SERIAL(CRoot, CObject, VERSIONABLE_SCHEMA | 1) expands to
//
CObject* PASCAL CRoot::CreateObject()
{
         return new CRoot;
}
 
extern AFX_CLASSINIT _init_CRoot;
 
AFX_CLASSINIT _init_CRoot (RUNTIME_CLASS(CRoot));
 
CArchive& AFXAPI operator >> (CArchive& ar, CRoot * &pOb)
{
         pOb = (CRoot *)ar.ReadObject(RUNTIME_CLASS(CRoot));
         return ar;
}
 
AFX_COMDAT CRuntimeClass CRoot::classCRoot =
{
         "CRoot", // Name of the class
         sizeof(class CRoot), // size
         VERSIONABLE_SCHEMA | 1, // schema
         CRoot::CreateObject, // pointer to CreateObject function used to intantiate object
         RUNTIME_CLASS(CObject), // Base class runtime information
         NULL, // linked list of the next class always NULL
         &_init_CRoot // pointer to AFX_CLASSINIT structure
};
 
CRuntimeClass* CRoot::GetRuntimeClass() const
{
         return RUNTIME_CLASS(CRoot);
}

这里没有插入运算符 <<,因为 CArchive 通过在全局命名空间中声明的基类指针来存储 CObject 派生类。

CArchive& AFXAPI operator<<(CArchive& ar, const CObject* pOb);

普通旧数据的处理相当直接。这里是一个读写 float 数据类型的例子

//
// Storing
//
CArchive& CArchive::operator<<(float f)
{ 
         if(!IsStoring())
                 AfxThrowArchiveException(CArchiveException::readOnly,m_strFileName);
         if (m_lpBufCur + sizeof(float) > m_lpBufMax) 
                 Flush();
         *(UNALIGNED float*)m_lpBufCur = f; // Write float into the byte array 
         m_lpBufCur += sizeof(float);       // Increment buffer pointer by the size of the float
         return *this;
}

以下代码是 float 数据类型的加载代码

//
// Loading
//
CArchive& CArchive::operator>>(float& f)
{ 
         if(!IsLoading())
                 AfxThrowArchiveException(CArchiveException::writeOnly,m_strFileName);
         if (m_lpBufCur + sizeof(float) > m_lpBufMax)
                 FillBuffer(UINT(sizeof(float) - (m_lpBufMax - m_lpBufCur)));
         f = *(UNALIGNED float*)m_lpBufCur; // Assign byte array to float size of the float
         m_lpBufCur += sizeof(float);       // Increment buffer pointer by the size of the float
         return *this; 
}

读写 CObject 派生类要复杂一些。这将在下一节中介绍。

注意事项

因为所有数据都存储在一个连续的字节缓冲区中,所以必须以与存储时完全相同的顺序读取。否则,在加载过程中将抛出 CArchiveException 异常。

为什么我不能多次调用 ar.GetObjectSchema()?

简而言之,你不能在每次加载对象时多次调用 GetObjectSchema,原因如下。

//
// GetObjectSchema
//
UINT CArchive::GetObjectSchema()
{
	UINT nResult = m_nObjectSchema;
	m_nObjectSchema = (UINT)-1; // can only be called once per Serialize
	return nResult;
}

至于为什么会这样?我最好的猜测是历史遗留问题。成员变量 CArchive::m_nObjectSchemaCRuntimeClass::m_wSchema 非常不同,因为 CArchive 对象架构是从文件中读取的,该文件可能包含许多具有不同架构的对象。它保存的是当前正在从文件中读取的对象的架构。想一想,当你反序列化一个对象时,例如在下面的例子中(假设 m_nObjectSchema 保持不变)

void CMyClass::Serialize(CArchive& ar)
{
         if (ar.IsStoring())
         {
                 // omitted storing code …
         }
         else
         {                
                 // Loading
                 UINT nSchema = ar.GetObjectSchema();
                 switch(nSchema)
                 {
                 case 1:
                          ar >> m_pObject1; // Version schema 10. Serialize may call GetObjectSchema
                          ar >> m_pObject2; // Version schema 1.  Serialize may call GetObjectSchema
                          ar >> m_pObject3; // Version schema 2.  Serialize may call GetObjectSchema
                          ar >> m_pObject4; // Version schema 15. Serialize may call GetObjectSchema
                 }
         }
 
         // For whatever reason
         if(ar.IsLoading())
         {
                 UINT nSchema = ar.GetObjectSchema(); // schema of this class?
         }
}

在上面的例子中,当你完成加载代码部分时,对象架构已经被改变了 4 次。我猜想,为了消除这种微妙的错误行为,MFC 框架决定从源头上杜绝,而不是让程序员为他们宝贵的数据被破坏而抓耳挠腮。

GetObjectSchema 每次加载对象只能被调用一次,因为框架在每次调用 CArchive::GetObjectSchema 后会强制将其重置为 (UINT)-1。

即使在今天的 MFC 库中,上面的例子也是万无一失的。来自 CArchive::ReadObject 的代码清单有以下代码

//
// CObject* CArchive::ReadObject(const CRuntimeClass* pClassRefRequested)
//
 
//... omitted code
 
TRY
{
         // allocate a new object based on the class just acquired
         pOb = pClassRef->CreateObject();
//... omitted code
         // Serialize the object with the schema number set in the archive
         UINT nSchemaSave = m_nObjectSchema; // Save current schema
         m_nObjectSchema = nSchema; // put new schema into the CArchive::m_nObjectSchema
         pOb->Serialize(*this); // Call virtual Serialize
         m_nObjectSchema = nSchemaSave; // Pop the saved schema back
}

如你所见,它将当前的 m_nObjectSchema 保存到 nSchemaSave 中。将当前对象的架构赋给 m_nObjectSchema。调用 Serialize。将保存的架构弹回到 m_nObjectSchema 中。因此,对象架构永远不会出错。

序列化基类和派生类

在 MFC 中,有四种方法可以解决派生类和基类的序列化问题。

但首先,让我们看看这个微妙的问题。在 16 位 MFC 实现的时代,磁盘空间和 RAM 一样是宝贵的商品。因此,无论你的类层次结构中有多少个派生类,它们的对象架构都将始终等于最终子类的架构,并且只会被写入一次!

//
// 
//
class CBase : public CObject
{
         DECLARE_SERIAL(CBase)
public:
         int m_i;
         float m_f;
         double m_d;
 
         virtual void Serialize(CArchive& ar);
};
 
class CDerived : public CBase
{
         DECLARE_SERIAL(CDerived)
public:
         long m_l;
         unsigned short m_us;
         long long m_ll;
 
         virtual void Serialize(CArchive& ar);
};
 
// Base class version
IMPLEMENT_SERIAL(CBase, CObject, VERSIONABLE_SCHEMA | 1) // Useless schema number. Never written to the file!
 
void CBase::Serialize(CArchive& ar)
{
         if (ar.IsStoring())
         {        // storing code omitted
         }
         else
         {        // loading code
                 UINT nSchema = ar.GetObjectSchema();
 
                 // oh no! nSchema = 2              
 
                 switch (nSchema)
                 {
                 case 1:
                          ar >> m_i;
                          ar >> m_f;
                          ar >> m_d;
                          break;
                 }
         }
}
 
// Derived class version
IMPLEMENT_SERIAL(CDerived, CBase, VERSIONABLE_SCHEMA | 2) // actual schema that is written to the file
 
void CDerived::Serialize(CArchive& ar)
{
         CBase::Serialize(ar);
 
         if (ar.IsStoring())
         {        // storing code omitted
         }
         else
         {        // loading code
                 UINT nSchema = ar.GetObjectSchema();
 
                 // oh no! nSchema = (UINT)-1 because of 2<sup>nd</sup> call to GetObjectSchema
 
                 switch (nSchema)
                 {
                 case 1:
                 case 2:
                          ar >> m_l;
                          ar >> m_us;
                          ar >> m_ll;
                          break;
                 }
         }
}

这是为什么呢?快速查看二进制文件转储会发现,对于 CSerializableDerived 类,架构只写入一次,并且它总是等于实例化的对象架构。在这种情况下,它等于 CSerializableDerived 类的架构,即使基类架构等于其他值。

跟踪到 CArchive::WriteObject 会发现这段代码

//
// void CArchive::WriteObject(const CObject* pOb)
//
 
// … omitted code
 
// write class of object first
CRuntimeClass* pClassRef = pOb->GetRuntimeClass(); // Contains m_wSchema of the CSerializableDerived which = 2
WriteClass(pClassRef);
 
// … omitted code
 

跟踪到 CArchive::WriteClass,框架首先写入一个等于 0xFFFF 的 WORD 值 wNewClassTag。然后它调用 CRuntimeClass::Store 函数

//
// void CArchive::WriteClass(const CRuntimeClass* pClassRef)
//
 
// … omitted code
 
// store new class
*this << wNewClassTag; // New class tag = 0xFFFF
pClassRef->Store(*this);
 
// … omitted code
 

CRuntimeClass::Store 函数获取类名的长度,并写入对象架构,然后是类名的长度和类名本身。这就是为什么对于最派生的类,对象架构只写入一次的问题的答案。

//
// 
//
void CRuntimeClass::Store(CArchive& ar) const
         // stores a runtime class description
{
         WORD nLen = (WORD)AtlStrLen(m_lpszClassName); // Get the length of the class name
         ar << (WORD)m_wSchema << nLen;                // Write schema followed by length of the class name into the file. Written only once!!!
         ar.Write(m_lpszClassName, nLen*sizeof(char)); // Write class name into the file
}

CRuntimeClass 信息被写入文件后,框架最终调用我们对象的虚函数 Serialize

//
// void CArchive::WriteObject(const CObject* pOb)
//
 
// … omitted code
 
// cause the object to serialize itself
((CObject*)pOb)->Serialize(*this);
 
// … omitted code
 

在对象加载期间发生的情况正好相反。首先调用提取运算符。此运算符由 IMPLEMENT_SERIAL 宏提供。

//
// Global extraction operator call provided by the IMPLEMENT_SERIAL macro
//
CArchive& AFXAPI operator >> (CArchive& ar, CSerializableDerived* &pOb)
{
         pOb = (CSerializableDerived*)ar.ReadObject(RUNTIME_CLASS(CSerializableDerived));
         return ar;
}

跟踪到 CArchive::ReadObject,我们会发现以下代码

//
// CObject* CArchive::ReadObject(const CRuntimeClass* pClassRefRequested)
//
 
// ... omitted code
 
 
// attempt to load next stream as CRuntimeClass
UINT nSchema;
DWORD obTag;
CRuntimeClass* pClassRef = ReadClass(pClassRefRequested, &nSchema, &obTag);
 
// ... omitted code

CArchive::ReadClass 函数首先读取对象标签

//
// CRuntimeClass* CArchive::ReadClass(const CRuntimeClass* pClassRefRequested,
//       UINT* pSchema, DWORD* pObTag)
//
 
// ... omitted code
 
// read object tag - if prefixed by wBigObjectTag then DWORD tag follows
DWORD obTag;
WORD wTag;
*this >> wTag; // Read the object tag
if (wTag == wBigObjectTag)
         *this >> obTag;
else
         obTag = ((wTag & wClassTag) << 16) | (wTag & ~wClassTag);
 
// ... omitted code
 
CRuntimeClass* pClassRef;
UINT nSchema;
if (wTag == wNewClassTag)
{
         // defined as follows
         // #define wNewClassTag    ((WORD)0xFFFF)      // special tag indicating new CRuntimeClass
 
         // new object follows a new class id
         if ((pClassRef = CRuntimeClass::Load(*this, &nSchema)) == NULL) // Read CRuntimeClass information from the file
                 AfxThrowArchiveException(CArchiveException::badClass, m_strFileName);
 
         // ... omitted code
}
// ... omitted code
 

以下是 CRuntimeClass::Load 函数的清单。请注意,类名不能超过 64 个字符。如果类名的长度大于或等于 64 个字符,或者 CArchive::Read 未能从文件中读取类名,则函数返回 NULL。如果类名成功从文件中读取,szClassName 将在 nLen 长度值处以 NULL 结尾,并在 CRuntimeClass::FromName 中进行查找

//
// 
//
CRuntimeClass* PASCAL CRuntimeClass::Load(CArchive& ar, UINT* pwSchemaNum)
         // loads a runtime class description
{
         if(pwSchemaNum == NULL)
         {
                 return NULL;
         }
         WORD nLen;
         char szClassName[64];
 
         WORD wTemp;
         ar >> wTemp; *pwSchemaNum = wTemp; // Read the schema
         ar >> nLen; // Read the length of the class name
 
         // load the class name
         if (nLen >= _countof(szClassName) ||
                 ar.Read(szClassName, nLen*sizeof(char)) != nLen*sizeof(char))
         {
                 return NULL;
         }
         szClassName[nLen] = '\0';
 
         // match the string against an actual CRuntimeClass
         CRuntimeClass* pClass = FromName(szClassName);
         if (pClass == NULL)
         {
                 // not found, trace a warning for diagnostic purposes
                 TRACE(traceAppMsg, 0, "Warning: Cannot load %hs from archive.  Class not defined.\n",
                          szClassName);
         }
 
         return pClass;
}
 

CRuntimeClass::FromName 只是遍历 AFX_MODULE_STATE::m_classList 并按名称进行比较。如果找到该类,则返回 CRuntimeClass 指针。AFX_MODULE_STATE CRuntimeClass 的发现是另一个值得单独写一篇文章的课题。但足以说明的是,这个特性是在 RTTI(运行时类型信息)编译器支持之前实现的,它允许在关闭 RTTI 编译器开关的情况下对 MFC 类进行运行时类型发现。事实上,Visual C++ 6.0 的默认设置就是关闭 RTTI 开关。

//
// 
//
CRuntimeClass* PASCAL CRuntimeClass::FromName(LPCSTR lpszClassName)
{
         CRuntimeClass* pClass=NULL;
 
         ENSURE(lpszClassName);
 
         // search app specific classes
         AFX_MODULE_STATE* pModuleState = AfxGetModuleState();
         AfxLockGlobals(CRIT_RUNTIMECLASSLIST);
         for (pClass = pModuleState->m_classList; pClass != NULL;
                 pClass = pClass->m_pNextClass)
         {
                 if (lstrcmpA(lpszClassName, pClass->m_lpszClassName) == 0)
                 {
                          AfxUnlockGlobals(CRIT_RUNTIMECLASSLIST);
                          return pClass;
                 }
         }
         AfxUnlockGlobals(CRIT_RUNTIMECLASSLIST);
#ifdef _AFXDLL
         // search classes in shared DLLs
         AfxLockGlobals(CRIT_DYNLINKLIST);
         for (CDynLinkLibrary* pDLL = pModuleState->m_libraryList; pDLL != NULL;
                 pDLL = pDLL->m_pNextDLL)
         {
                 for (pClass = pDLL->m_classList; pClass != NULL;
                          pClass = pClass->m_pNextClass)
                 {
                          if (lstrcmpA(lpszClassName, pClass->m_lpszClassName) == 0)
                          {
                                   AfxUnlockGlobals(CRIT_DYNLINKLIST);
                                   return pClass;
                          }
                 }
         }
         AfxUnlockGlobals(CRIT_DYNLINKLIST);
#endif
 
         return NULL; // not found
}
 

回到 CArchive::ReadClass,它返回 CRuntimeClasspSchemapObTag 指针。

//
// 
//CRuntimeClass* CArchive::ReadClass(const CRuntimeClass* pClassRefRequested,
//       UINT* pSchema, DWORD* pObTag)
 
//... omitted code
 
 
// store nSchema for later examination
if (pSchema != NULL)
         *pSchema = nSchema;
else
         m_nObjectSchema = nSchema; 
 
// store obTag for later examination
if (pObTag != NULL)
         *pObTag = obTag;
 
// return the resulting CRuntimeClass*
return pClassRef;
 
 

在成功获取 CRuntimeClass 指针后,框架调用由 DECLARE_SERIALIMPLEMENT_SERIAL 宏提供的 CreateObject

  • 将当前的 CArchive::m_nObjectScema 存储到 nSchemaSave
  • 将当前 CRuntimeClass 的架构赋给 CArchive::m_nObjectSchema
  • 调用虚函数 Serialize
  • nSchemaSave 弹回到 CArchive::m_nObjectSchema
//
// CObject* CArchive::ReadObject(const CRuntimeClass* pClassRefRequested)
//
 
//... omitted code
 
TRY
{
         // allocate a new object based on the class just acquired
         pOb = pClassRef->CreateObject();
         
//... omitted code
         // Serialize the object with the schema number set in the archive
         UINT nSchemaSave = m_nObjectSchema; // Save current schema
         m_nObjectSchema = nSchema; // put new schema into the CArchive::m_nObjectSchema
         pOb->Serialize(*this); // Call virtual Serialize
         m_nObjectSchema = nSchemaSave; // Pop the saved schema back
         ASSERT_VALID(pOb);
}

所以现在你知道为什么你的类只有一个架构,无论你的类层次结构中有多少个类。

我们如何解决这个问题?有四种方法可以解决。有些方法比其他方法更优雅。让我们来看看所有这些方法。当然,这只适用于你必须在所有类中维护版本的情况。最简单的方法是不对任何东西进行版本控制,但在现实生活中,如果你的应用程序预期寿命以十年计,那么从一开始就维护版本控制是绝对必要的。

第一种解决方案:在派生类中完成所有序列化

这不是一个优雅的解决方案,但它行之有效,并且消除了所有意外。对于我们上面的例子,代码将如下所示

 
// Derived class version
IMPLEMENT_SERIAL(CDerived, CBase, VERSIONABLE_SCHEMA | 2)
 
void CDerived::Serialize(CArchive& ar)
{
         // Do not call base class
         // CBase::Serialize(ar);
 
         if (ar.IsStoring())
         {        // storing code
                 // serialize base members
                 ar << m_i;
                 ar << m_f;
                 ar << m_d;
 
                 // serialize this class members
                 ar << m_l;
                 ar << m_us;
                 ar << m_ll;
 
         }
         else
         {        // loading code
                 UINT nSchema = ar.GetObjectSchema();
 
                 
                 switch (nSchema)
                 {
                 case 1:
                 case 2:
                          // deserialize base members
                          ar >> m_i;
                          ar >> m_f;
                          ar >> m_d;
 
                          // deserialize this class members
                          ar >> m_l;
                          ar >> m_us;
                          ar >> m_ll;
                          break;
                 }
         }
}

这个解决方案不是很漂亮。如果你的基类有太多成员,你的 Serialize 函数可能会变得非常庞大。

第二种解决方案:将架构弹回 CArchive 中

这个解决方案稍微优雅一些,但是当架构发生变化时,你仍然需要在所有基类中增加架构版本号。

// Base class version
IMPLEMENT_SERIAL(CBase, CObject, VERSIONABLE_SCHEMA | 1) // Useless schema number
 
void CBase::Serialize(CArchive& ar)
{
         if (ar.IsStoring())
         {        // storing code omitted
         }
         else
         {        // loading code
                 UINT nSchema = ar.GetObjectSchema();
 
                 // oh no, nSchema = 2              
 
                 switch (nSchema)
                 {
                 case 1:
                 case 2: // THIS IS REQUIRED!!!
                          ar >> m_i;
                          ar >> m_f;
                          ar >> m_d;
                          break;
                 }
 
                 // Pop the schema back into the CArchive for derived class to use
                 ar.SetObjectSchema(nSchema);
         }
}
 
// Derived class version
IMPLEMENT_SERIAL(CDerived, CBase, VERSIONABLE_SCHEMA | 2)
 
void CDerived::Serialize(CArchive& ar)
{
         // Call base class
         CBase::Serialize(ar);
 
         if (ar.IsStoring())
         {        // storing code omitted
         }
         else
         {        // loading code
                 UINT nSchema = ar.GetObjectSchema();
 
                 switch (nSchema)
                 {
                 case 1:
                 case 2:
                          ar >> m_l;
                          ar >> m_us;
                          ar >> m_ll;
                          break;
                 }
         }
}

第三种解决方案:考虑使用“不要调用我们,我们会调用您”设计模式来重构 Serialize 函数

添加 private virtual function SerializeImpl(CArchive& ar, UINT nSchema) 将消除多次调用 CArchive::GetObjectSchema 的需要。

//
// 
//
class CBase : public CObject
{
	DECLARE_SERIAL(CBase)
public:
	int m_i;
	float m_f;
	double m_d;

	virtual void Serialize(CArchive& ar);

private:
	virtual void SerializeImpl(CArchive& ar, UINT nSchema);
};

class CDerived : public CBase
{
	DECLARE_SERIAL(CDerived)
public:
	long m_l;
	unsigned short m_us;
	long long m_ll;

private:
	virtual void SerializeImpl(CArchive& ar, UINT nSchema);
};

// Base class version
IMPLEMENT_SERIAL(CBase, CObject, VERSIONABLE_SCHEMA | 1) // Useless schema number

void CBase::Serialize(CArchive& ar)
{
	if (ar.IsStoring())
	{	// storing code omitted
		// CDerived::SerializeImpl version will be called
		SerializeImpl(ar, (UINT)-1);
	}
	else
	{	// loading code
		UINT nSchema = ar.GetObjectSchema();
		switch (nSchema)
		{
		case 1:
		case 2: // THIS IS STILL REQUIRED!!!
			ar >> m_i;
			ar >> m_f;
			ar >> m_d;
			break;
		}

 		// CDerived::SerializeImpl version will be called
		SerializeImpl(ar, nSchema);
	}
}

void CBase::SerializImpl(CArchive& ar, UINT nSchema)
{
	// Not implemented	
}




// Derived class version
IMPLEMENT_SERIAL(CDerived, CBase, VERSIONABLE_SCHEMA | 2)

// Eliminates calling to ar.GetObjectSchema() alltogether
void CDerived::SerializImpl(CArchive& ar, UINT nSchema)
{
	// call base if you have more than 2 parent classes
	// so the parent’s class serialization routine utilized
	CBase::SerializImpl(ar, nShema);

	if (ar.IsStoring())
	{	// storing code omitted
	}
	else
	{	// loading code
		switch (nSchema)
		{
		case 1:
		case 2:
			ar >> m_l;
			ar >> m_us;
			ar >> m_ll;
			break;
		}
	}
}

这在某种程度上更为优雅,但当架构发生变化时,仍然需要我们在所有基类中增加版本号。

接下来是最优雅的解决方案。

第四种解决方案:将基类架构存储为类的第一个成员

现在,这个解决方案解决了 MFC 序列化机制的缺点。你可以通过成员变量静态的 classCBase::m_wSchema 访问你的基类架构,在我们的例子中是这样。

// Base class version
IMPLEMENT_SERIAL(CBase, CObject, VERSIONABLE_SCHEMA | 1) // Not so useless schema number

void CBase::Serialize(CArchive& ar)
{
	if (ar.IsStoring())
	{	// storing code

		// store the classCBase.m_wSchema; Added by the DECLARE_SERIAL macro and populated by the IMPLEMENT_SERIAL macro
		// as the very 1st member
		WORD wSchema = (WORD)classCBase.m_wSchema; // Strips VERSIONABLE_SCHEMA and Equals 1 as declared above
		ar << wSchema;
		ar << m_i;
		ar << m_f;
		ar << m_d;
	}
	else
	{	// loading code
		
		// Do not call CArchive::GetObjectSchema!
		//UINT nSchema = ar.GetObjectSchema();
		// Read base object schema
		WORD wSchema = 0;
		ar >> wSchema;
		switch (wSchema) // Equals 1
		{
		case 1:
			ar >> m_i;
			ar >> m_f;
			ar >> m_d;
			break;
		}
	}
}


// Derived class version
IMPLEMENT_SERIAL(CDerived, CBase, VERSIONABLE_SCHEMA | 2)

void CDerived:: Serialize(CArchive& ar)
{
	CBase::Serialize(ar);

	if (ar.IsStoring())
	{	// storing code omitted
	}
	else
	{	// loading code
		UINT nSchema = ar.GetObjectSchema(); // equals 2
		switch (nSchema)
		{
		case 1:
		case 2:
			ar >> m_l;
			ar >> m_us;
			ar >> m_ll;
			break;
		}
	}
}

这是最优雅的解决方案,因为它让你无需维护基类,代价是每个父类在文件中增加一个 `sizeof(WORD)` 的大小。

序列化纯基类

假设你有一个带有纯虚函数的 CObject 派生类。

//
// CObject derived class with pure virtual functions
//
class CPureBase : public CObject
{
         DECLARE_SERIAL(CPureBase)
public:
         CPureBase();
         virtual ~CPureBase();
         virtual void Serialize(CArchive& ar);
 
         virtual CString CanSerialize() const = 0;
         virtual CString GetObjectSchema() const = 0;
         virtual CString GetObjectRunTimeName() const = 0;
};

在正常情况下,这是行不通的,因为 IMPLEMENT_SERIAL 宏会向你的代码中添加以下函数

//
// Function added by IMPLEMENT_SERIAL macro
//
CObject* PASCAL CPureBase::CreateObject()
{ 
         return new CPureBase; // Compiler error! Cannot instantiate class due to pure virtual functions
}

为了解决这个问题,我们需要创建我们自己版本的 IMPLEMENT_SERIAL 宏,让它从 CreateObject 函数返回 nullptr。

//
// Helper macro for Pure base serializable classes
// Removes instancing of the new class in CreateObject()
#define IMPLEMENT_SERIAL_PURE_BASE(class_name, base_class_name, wSchema)\
         CObject* PASCAL class_name::CreateObject() \
                 { return nullptr; } \
         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; }

现在你可以声明你的纯基类是可序列化的了。

//
// This code compiles
//
IMPLEMENT_SERIAL_PURE_BASE(CPureBase, CObject, VERSIONABLE_SCHEMA | 1)
 
// CPureBase
CPureBase::CPureBase()
{
}
 
CPureBase::~CPureBase()
{
}
 
// CPureBase member functions
void CPureBase::Serialize(CArchive& ar)
{
         if (ar.IsStoring())
         {        // storing code
         }
         else
         {        // loading code
         }
}

使用文档/视图进行序列化

这种类型的序列化在 MFC 文献中被提及最多。如果你的应用程序具有文档视图架构,序列化已经是 CDocument 派生类的一部分。Serialize 的重写提供了必要的代码。典型的代码结构如下所示

 
void CSerializeDemoDoc::Serialize(CArchive& ar)
{
         if (ar.IsStoring())
         {
                 // Storing
                 ar << m_pRoot;
         }
         else
         {
                 // Loading
                 ar >> m_pRoot;
         }
}

不使用文档/视图进行序列化

要在没有文档/视图的情况下进行序列化,比如说在控制台应用程序中,你需要添加以下代码来写入文件

//
// Writing to the file
//
CFile file; // Create CFile object
 
// Open CFile object
if (!file.Open(_T("Test.my_ext"), CFile::modeCreate | CFile::modeReadWrite | CFile::shareExclusive))
         return false;
 
// Create CArchive object pass a pointer to CFile and , CArchive::store enumeration
CArchive ar(&file, CArchive::store | CArchive::bNoFlushOnDelete);
 
// write your value
ar << val;
 
// Close CArchive object
ar.Close();
 
// Close CFile object
file.Close();
 

要在没有文档/视图的情况下反序列化或读取,请使用以下代码

//
// Reading from a file
//
CFile file;
 
if (!file.Open(_T("Test.my_ext"), CFile::modeRead | CFile::shareExclusive))
         return false;
 
CArchive ar(&file, CArchive::load);
 
ar >> val;
 
ar.Close();
file.Close();

只需几行代码,你就驾驭了 CArchive 对象的强大功能。

序列化普通旧数据类型

CArchive 提供了以下插入和提取运算符来处理普通旧数据的存储和检索。

//
// CArchive operators
//
// insertion operations
         CArchive& operator<<(BYTE by);
         CArchive& operator<<(WORD w);
         CArchive& operator<<(LONG l);
         CArchive& operator<<(DWORD dw);
         CArchive& operator<<(float f);
         CArchive& operator<<(double d);
         CArchive& operator<<(LONGLONG dwdw);
         CArchive& operator<<(ULONGLONG dwdw);
 
         CArchive& operator<<(int i);
         CArchive& operator<<(short w);
         CArchive& operator<<(char ch);
#ifdef _NATIVE_WCHAR_T_DEFINED
         CArchive& operator<<(wchar_t ch);
#endif
         CArchive& operator<<(unsigned u);
 
         template < typename BaseType , bool t_bMFCDLL>
         CArchive& operator<<(const ATL::CSimpleStringT<BaseType, t_bMFCDLL>& str);
 
         template< typename BaseType, class StringTraits >
         CArchive& operator<<(const ATL::CStringT<BaseType, StringTraits>& str);
 
         template < typename BaseType , bool t_bMFCDLL>
         CArchive& operator>>(ATL::CSimpleStringT<BaseType, t_bMFCDLL>& str);
 
         template< typename BaseType, class StringTraits >
         CArchive& operator>>(ATL::CStringT<BaseType, StringTraits>& str);
 
         CArchive& operator<<(bool b);
 
         // extraction operations
         CArchive& operator>>(BYTE& by);
         CArchive& operator>>(WORD& w);
         CArchive& operator>>(DWORD& dw);
         CArchive& operator>>(LONG& l);
         CArchive& operator>>(float& f);
         CArchive& operator>>(double& d);
         CArchive& operator>>(LONGLONG& dwdw);
         CArchive& operator>>(ULONGLONG& dwdw);
 
         CArchive& operator>>(int& i);
         CArchive& operator>>(short& w);
         CArchive& operator>>(char& ch);
#ifdef _NATIVE_WCHAR_T_DEFINED
         CArchive& operator>>(wchar_t& ch);
#endif
         CArchive& operator>>(unsigned& u);
         CArchive& operator>>(bool& b);
...

如果你需要序列化未在 CArchive 对象中声明的数据类型,你需要编写自己的实现。稍后当我介绍序列化 Windows SDK 结构时,我们会再讨论这个问题。

序列化 CArray 模板集合

MFC 为其几乎所有的集合提供了序列化支持,为了序列化 MFC 集合,你所需要做的就是调用集合版本的 Serialize(CArchive& ar)CArray 有所不同,因为它是一个模板,并且模板类型事先未知。而且该类型可能派生自 CObject,也可能没有。CArray::Serialize 函数的默认实现如下所示。它所做的只是在写入操作期间写入 CArray 的大小,在读取操作期间从磁盘读取 CArray 的大小并调整 CArray 的大小。然后它会友好地将调用转发给 SerializeElements<TYPE>() 函数。

//
// Serialize function forwards call to the SerializeElements<TYPE>()
//
template<class TYPE, class ARG_TYPE>
void CArray<TYPE, ARG_TYPE>::Serialize(CArchive& ar)
{
         ASSERT_VALID(this);
 
         CObject::Serialize(ar);
         if (ar.IsStoring())
         {
                 // Just writes the collection size
                 ar.WriteCount(m_nSize);
         }
         else
         {
                 // Just reads the collection size and resizes the CArray
                 DWORD_PTR nOldSize = ar.ReadCount();
                 SetSize(nOldSize, -1);
         }
         SerializeElements<TYPE>(ar, m_pData, m_nSize);
}

用户必须为正在存储或从归档中检索的类型提供 SerializeElements<TYPE>() 的适当实现。以下清单演示了 CAge 类的 SerializeElements<TYPE> 实现。有关实现细节,请参阅 SerializeDemo 项目。

//
// 
//
class CAge : public CObject
{
         DECLARE_SERIAL(CAge)
public:
         CAge();
         CAge(int nAge);
         virtual ~CAge();
         virtual void Serialize(CArchive& ar);
 
         UINT m_nAge;
};
 
 
// CArray serialization helper specialized for the CAge class
template<> inline void AFXAPI SerializeElements(CArchive& ar, CAge** pAge, INT_PTR nCount)
{
         for (INT_PTR i = 0; i < nCount; i++, pAge++) 
         {
                 if (ar.IsStoring())
                 {
                          ar << *pAge; // Calls CArchive::WriteObject
                 }
                 else
                 {
                          CAge* p = nullptr;
                          ar >> p; // Calls CArchive::ReadObject
                          *pAge = p;
                 }
         }
}

序列化到进程内存以及从进程内存序列化

通过 CMemFile 支持与内存之间的序列化。CMemFile 不需要文件名。

//
// Write to the memory
//
CMemFile file;
CArchive ar(&file, CArchive::store);
 
ar << val;
 
ar.Close();
 

从内存中序列化的方式如下

//
// Read from the memory
//
CMemFile file;
 
// CByteArray aBytes declared and populated elsewhere
file.Attach(m_aBytes.GetData(), m_aBytes.GetSize());
CArchive ar(&file, CArchive::load);
 
ar >> val;
 
ar.Close();
 

序列化到共享进程内存以及从共享进程内存序列化

通过 CSharedFile 支持与内存之间的序列化。如果你想将序列化的对象传输到剪贴板,以便粘贴到应用程序的另一个实例中,或者传递给另一个应用程序,这将非常有用。

//
// Write to the shared memory to do a clipboard copy operation
//
UINT m_nClipboardFormat = RegisterClipboardFormat(_T("MY_APP_DATA"));
 
CSharedFile file(GMEM_MOVEABLE | GMEM_SHARE | GMEM_ZEROINIT);
CArchive ar(&file, CArchive::store | CArchive::bNoFlushOnDelete);
 
// CView derived class
GetDocument()->Serialize(ar);      
 
EmptyClipboard();
SetClipboardData(m_nClipboardFormat, file.Detach());
CloseClipboard();
 
ar.Close();
file.Close();

从共享内存序列化,从剪贴板粘贴操作

//
// Read from the shared memory clipboard paste
//
 
UINT m_nClipboardFormat = RegisterClipboardFormat(_T("MY_APP_DATA"));
 
if (!OpenClipboard())
         return;
 
CSharedFile file(GMEM_MOVEABLE | GMEM_SHARE | GMEM_ZEROINIT);
         
HGLOBAL hMem = GetClipboardData(m_nClipboardFormat);
 
if (hMem == nullptr)
{
         CloseClipboard();
         return;
}
 
file.SetHandle(hMem);
 
CArchive ar(&file, CArchive::load);
 
// CView derived class
GetDocument()->DeleteContents();
GetDocument()->Serialize(ar);
 
CloseClipboard();
 
ar.Close();
file.Close();
 

序列化到套接字以及从套接字序列化

与套接字之间的序列化是通过 CSocketFile 类完成的。只有当 CSocket 的类型为 SOCK_STREAM 时,你才能将 CArchive 序列化到 CSocket 中。这个主题比 MSDN 文档中描述的要复杂一些。官方文档描述了你可以使用 CSocketFileCSocket 进行读写。这对于写操作是正确的,但对于读操作则不一定正确。如果你传输的数据大小只有几个字节,那么是的,你可以使用 CSocketFile 来接收数据。但是,如果你的数据大小是兆字节级别(或任何大于读取缓冲区的大小),那么你很可能会分几次读取来接收数据,你将不得不先将所有数据累积到 CByteArray 结构中,只有在所有数据都接收完毕后,你才能将其附加到 CMemFile 而不是 CSocketFile 并进行反序列化。尝试从 CSocketFile 读取部分数据通常会导致 CArchiveException

//
// Write to the socket. In this case using CSocketFile is fine
//
CSocket sock;
 
if (!sock.Create()) // Defaults to the SOCK_STREAM
         return;
 
// Assuming there is a server running on the local host port 1011
if (!sock.Connect(_T("127.0.0.1"), 1011))
         return;
 
CSocketFile file(&sock);
CArchive ar(&file, CArchive::store | CArchive::bNoFlushOnDelete);
 
// Write value to the socket
ar << m_pRoot;
 
ar.Close();
file.Close();
sock.Close();
 

从套接字进行序列化要复杂一些。我给出了类的完整清单,以演示如何正确地从套接字读取大的二进制数据集。有关完整的源代码清单,请参阅示例项目 SerializeTcpServer

//
// Read from the socket. In this case using CSocketFile will not work 
// if the transmitted data is greater that the receiving buffer size
//
 
// CSocket derived class declaration
class CSockThread;
 
// CRecvSocket command target
class CRecvSocket : public CSocket
{
public:
         CRecvSocket();
         virtual ~CRecvSocket();
         virtual void OnReceive(int nErrorCode);
 
         CSockThread* m_pThread;   // Parent thread to ensure our server handles multiple connections simultaneously
         CByteArray m_aBytes;      // Array of received bytes
 
private:
         DWORD m_dwReads; // Number of reads
 
         void Display(CRoot* pRoot);
};
 
 
// Implementation
#define INCOMING_BUFFER_SIZE 65536
 
// CRecvSocket
CRecvSocket::CRecvSocket(): m_pThread(nullptr)
, m_dwReads(0)
{
}
 
CRecvSocket::~CRecvSocket()
{
}
 
// CRecvSocket member functions
void CRecvSocket::OnReceive(int nErrorCode)
{
         // yield 10 msec
         Sleep(10);
 
         // Our reading buffer
         BYTE btBuffer[INCOMING_BUFFER_SIZE] = { 0 };
 
         // Read from the socket size of our buffer size or less
         int nRead = Receive(btBuffer, INCOMING_BUFFER_SIZE);
 
         switch (nRead)
         {
         case 0:
                 // No data - Quit
                 m_pThread->PostThreadMessage(WM_QUIT, 0, 0);
                 break;
         case SOCKET_ERROR:
                 if (GetLastError() != WSAEWOULDBLOCK)
                 {
                          // Socket error - Quit
                          m_pThread->PostThreadMessage(WM_QUIT, 0, 0);
                 }
                 break;
         default:
                 // Increment read counter
                 m_dwReads++;
                 
                 // Read into the byte array
                 CByteArray aBytes;
 
                 // Resize our byte array to the size of the received data
                 aBytes.SetSize(nRead);
 
                 // Copy received data into the CByteArray
                 CopyMemory(aBytes.GetData(), btBuffer, nRead);
 
                 // Append received data to m_aBytes member
                 m_aBytes.Append(aBytes);
 
                 DWORD dwReceived = 0;
                 
                 // Look ahead for more incoming data
                 if (IOCtl(FIONREAD, &dwReceived))
                 {
                          // No more incoming data
                          if (dwReceived == 0)
                          {
                                   // We have received all of the incoming data
                          
                                   // Instead of CSocketFile use CMemFile
                                   CMemFile file;
                                   file.Attach(m_aBytes.GetData(), m_aBytes.GetSize());
                                   CArchive ar(&file, CArchive::load);
                                   CRoot* pRoot = nullptr;
 
                                   TRY
                                   {
                                            ar >> pRoot;
                                   }
                                   CATCH(CArchiveException, e)
                                   {
                                            std::cout << "Error reading data " << std::endl;
                                   }
                                   END_CATCH
                 
                                   if (pRoot)
                                   {
                                            // Use our de serialized CRoot class
                                            Display(pRoot);
                                            delete pRoot;
                                   }
                                   ar.Close();
                                   file.Close();
 
                                   // finally quit
                                   m_pThread->PostThreadMessage(WM_QUIT, 0, 0);
                          }
                 }                
         }
         CSocket::OnReceive(nErrorCode);
}

在当今的应用程序中,你很少会在一次 OnReceive 调用中接收到所有的二进制或文本数据传输。因此,你需要将所有数据累积到一个字节数组中。只有这样,你才能通过将累积的 CByteArray 附加到 CMemFile 来成功地反序列化它。上面的例子调用 IOCtl(FIONREAD, &dwReceived) 来确定是否有更多的数据正在传入。经验法则是:因为我们的读取缓冲区等于 65536 字节,任何传输大于读取缓冲区的数据都会导致不止一次的读取。

CSockThread* m_pThread; 的实现在示例项目 SerializeTcpServer 中提供。

序列化任意字节流

任意字节流基本上是任何你不知道或不关心其内部结构的二进制文件。一个例子是,你想在你的类数据中存储 JPEG 图像或 mpeg 4 电影文件,而不需要任何关于底层数据结构的知识。你可以在以后反序列化它,并用适当的应用程序使用它。MFC 序列化允许你轻松地存储这些数据。

在下面的代码中,我们将存储四张 JPEG 图片的字节流

//
// Declare class to hold an array of JPEG images
//
class CMyPicture : public CObject
{
         DECLARE_SERIAL(CMyPicture)
public:
         CMyPicture();
         virtual ~CMyPicture();
         virtual void Serialize(CArchive& ar);
 
         CString GetHeader() const;
 
         CString m_strName;        // Original image file name
         CString m_strNewName;     // New image file
         CByteArray m_bytes;       // Image binary data array
};

typedef CTypedPtrArray<CObArray, CMyPicture*> CMyPictureArray;

以下是该类的主体清单

//
// Class to store JPEG images
//
IMPLEMENT_SERIAL(CMyPicture, CObject, VERSIONABLE_SCHEMA | 1)
 
// CMyPicture
CMyPicture::CMyPicture()
{
}
 
CMyPicture::~CMyPicture()
{
}
 
// CMyPicture member functions
void CMyPicture::Serialize(CArchive& ar)
{
         if (ar.IsStoring())
         {        // storing code
                 ar << m_strName;
                 ar << m_strNewName;
         }
         else
         {        // loading code
                 UINT nSchema = ar.GetObjectSchema();
                 switch (nSchema)
                 {
                 case 1:
                          ar >> m_strName;
                          ar >> m_strNewName;
                          break;
                 }
         }
 
         // Serialize arbitrary byte stream into or from the file
         m_bytes.Serialize(ar);
}
 

要用 JPEG 图像数据填充这样一个类,你只需要做以下事情

//
// CMyPictureArray m_aPictures declared in the class header as a member
//
         m_aPictures.Add(InitPicture("Water lilies.jpg", "Water lilies Output.jpg"));
         m_aPictures.Add(InitPicture("Blue hills.jpg", "Blue hills Output.jpg"));
         m_aPictures.Add(InitPicture("Sunset.jpg", "Sunset Output.jpg"));
         m_aPictures.Add(InitPicture("Winter.jpg", "Winter Output.jpg"));
 
         UpdateAllViews(nullptr, HINT_GENERATED_DATA);
         SetModifiedFlag();
}
 
// Read binary stream from an unknown file
std::vector<BYTE> CSerializeDemoDoc::ReadBinaryFile(const char* filename)
{
         std::basic_ifstream<BYTE> file(filename, std::ios::binary);
         return std::vector<BYTE>((std::istreambuf_iterator<BYTE>(file)), std::istreambuf_iterator<BYTE>());
}
 
CMyPicture* CSerializeDemoDoc::InitPicture(const char* sFileName, const char* sOutFileName)
{
         std::vector<BYTE> vJPG = ReadBinaryFile(sFileName);
         CMyPicture* pPicture = new CMyPicture;
         pPicture->m_strName = sFileName;
         pPicture->m_strNewName = sOutFileName;
         pPicture->m_bytes.SetSize(vJPG.size());
         CopyMemory(pPicture->m_bytes.GetData(), (void*)&vJPG[0], vJPG.size() * sizeof(BYTE));
         return pPicture;
}
 
// Writes JPEG images back to the hard drive
void CSerializeDemoDoc::OnTestdataWriteimagedatatodisk()
{
         for (INT_PTR i = 0; i < m_pRoot->m_aPictures.GetSize(); i++)
         {
                 CMyPicture* pPic = m_pRoot->m_aPictures.GetAt(i);
 
                 std::ofstream fout(pPic->m_strNewName, std::ios::out | std::ios::binary);
                 fout.write((char*)pPic->m_bytes.GetData(), pPic->m_bytes.GetSize());
                 fout.close();
         }
 
         AfxMessageBox(_T("Finished writing images back to disk"), MB_ICONINFORMATION);
}

序列化 Windows SDK 数据结构

CArchive 类不提供 Windows SDK 结构的序列化。然而,为这种序列化添加支持几乎是毫不费力的。以下代码演示了如何序列化 LOGFONT SDK 结构。

//
// LOGFONT SDK structure serialization code
//
 
// LOGFONT write
inline CArchive& AFXAPI operator <<(CArchive& ar, const LOGFONT& lf)
{
         CString strFace(lf.lfFaceName);
 
         ar << lf.lfHeight;
         ar << lf.lfWidth;
         ar << lf.lfEscapement;
         ar << lf.lfOrientation;
         ar << lf.lfWeight;
         ar << lf.lfItalic;
         ar << lf.lfUnderline;
         ar << lf.lfStrikeOut;
         ar << lf.lfCharSet;
         ar << lf.lfOutPrecision;
         ar << lf.lfClipPrecision;
         ar << lf.lfQuality;
         ar << lf.lfPitchAndFamily;
         ar << strFace;
 
         return ar;
}
 
// LOGFONT read
inline CArchive& AFXAPI operator >> (CArchive& ar, LOGFONT& lf)
{
         CString strFace;
 
         ar >> lf.lfHeight;
         ar >> lf.lfWidth;
         ar >> lf.lfEscapement;
         ar >> lf.lfOrientation;
         ar >> lf.lfWeight;
         ar >> lf.lfItalic;
         ar >> lf.lfUnderline;
         ar >> lf.lfStrikeOut;
         ar >> lf.lfCharSet;
         ar >> lf.lfOutPrecision;
         ar >> lf.lfClipPrecision;
         ar >> lf.lfQuality;
         ar >> lf.lfPitchAndFamily;
         ar >> strFace;
         _tcscpy_s(lf.lfFaceName, strFace);
 
         return ar;
}

在你定义了 LOGFONT 的提取和插入运算符之后,你只需要执行以下代码片段。

//
// Serialize LOGFONT structure m_lf
//
 
void CRoot::Serialize(CArchive& ar)
{
         CBase::Serialize(ar);
 
         if (ar.IsStoring())
         {        // storing code
                 ar << m_lf; // Write LOGFONT
         }
         else
         {        // loading code
                 UINT nSchema = ar.GetObjectSchema();
                 switch (nSchema)
                 {
                 case 1:
                          ar >> m_lf; // Read LOGFONT
                          break;
                 }
         }
}

接下来的代码片段序列化了 WINDOWPLACEMENT SDK 结构

//
// Serializing WINDOWPLACEMENT
//
 
// WINDOWPLACEMENT write
inline CArchive& AFXAPI operator <<(CArchive& ar, const WINDOWPLACEMENT& val)
{
         ar << val.flags;
         ar << val.length;
         ar << val.ptMaxPosition.x;
         ar << val.ptMaxPosition.y;
         ar << val.ptMinPosition.x;
         ar << val.ptMinPosition.y;
         ar << val.rcNormalPosition.bottom;
         ar << val.rcNormalPosition.left;
         ar << val.rcNormalPosition.right;
         ar << val.rcNormalPosition.top;
         ar << val.showCmd;
 
         return ar;
}
 
// WINDOWPLACEMENT read
inline CArchive& AFXAPI operator >> (CArchive& ar, WINDOWPLACEMENT& val)
{
         ar >> val.flags;
         ar >> val.length;
         ar >> val.ptMaxPosition.x;
         ar >> val.ptMaxPosition.y;
         ar >> val.ptMinPosition.x;
         ar >> val.ptMinPosition.y;
         ar >> val.rcNormalPosition.bottom;
         ar >> val.rcNormalPosition.left;
         ar >> val.rcNormalPosition.right;
         ar >> val.rcNormalPosition.top;
         ar >> val.showCmd;
 
         return ar;
}

然后,读写 WINDOWPLACEMENT 结构就变得像这样简单了

//
// Reading and writing WINDOWPLACEMENT structure
//
void CRoot::Serialize(CArchive& ar)
{
         CBase::Serialize(ar);
 
         if (ar.IsStoring())
         {        // storing code
                 ar << m_wp; // Write WINDOWPLACEMENT struct
         }
         else
         {        // loading code
                 UINT nSchema = ar.GetObjectSchema();
                 switch (nSchema)
                 {
                 case 1:
                          ar >> m_wp; // Read WINDOWPLACEMENT struct
                          break;
                 }
         }
}

序列化 STL 集合

STL 集合的序列化就像 SDK 数据结构的序列化一样简单。让我们为流行的 STL 集合定义插入和提取运算符。要序列化 std::vector<int>,我们需要以下定义

//
// STL vector<int> write
//
inline CArchive& AFXAPI operator <<(CArchive& ar, const std::vector<int>& val)
{
	// first store the size of the vector
	ar << (int)val.size();
	for each (int k in val)
	{
		ar << k; // store each int into the file
	}
	return ar;
}

要将 STL vector 读回到 std::vector<int> 中,我们执行以下操作

//
// STL vector<int> read
//
inline CArchive& AFXAPI operator >> (CArchive& ar, std::vector<int>& val)
{
	int nSize;
	ar >> nSize; // read the vector size
	val.resize(nSize); // resize vector to the read size
	for (size_t i = 0; i < (size_t)nSize; i++)
	{
		ar >> val[i]; // retrieve values
	}
	return ar;
}

std::map<char, int> 集合的序列化。首先,我们存储 map 的大小。因为 std::map<char, int> 的底层元素是 std::pair<char, int>,所以我们存储 pair 的第一个和第二个成员。

//
// std::map<char, int> write
//
 
inline CArchive& AFXAPI operator <<(CArchive& ar, const std::map<char, int>& val)
{
         ar << (int)val.size();
         for each (std::pair<char, int> k in val)
         {
                 ar << k.first;
                 ar << k.second;
         }
         return ar;
}

std::map<char, int> 的读取代码如下。

//
// std::map<char, int> read 
// 
inline CArchive& AFXAPI operator >> (CArchive& ar, std::map<char, int>& val)
{
         int nSize;
         ar >> nSize;
         for (size_t i = 0; i < (size_t)nSize; i++)
         {
                 std::pair<char, int> k;
                 ar >> k.first;
                 ar >> k.second;
                 val.insert(k);
         }
         return ar;
}

STL 固定大小 std::array<int, 3> 的序列化。

//
// STL std::array<int, 3> write
//
inline CArchive& AFXAPI operator <<(CArchive& ar, const std::array<int, 3>& val)
{
         for each (int k in val)
         {
                 ar << k;
         }
         return ar;
}

std::array<int, 3> 读取运算符。

//
// STL std::array<int, 3> read
//
inline CArchive& AFXAPI operator >> (CArchive& ar, std::array<int, 3>& val)
{
         for (size_t i = 0; i < (size_t)val.size(); i++)
         {
                 ar >> val[i];
         }
         return ar;
}

std::set<std::string> 集合的序列化。

//
// STL std::set<std::string> write 
// 
inline CArchive& AFXAPI operator <<(CArchive& ar, const std::set<std::string>& val)
{
         ar << (int)val.size(); // write the size first
         for each (std::string k in val)
         {
                 ar << CStringA(k.c_str());
         }
         return ar;
}

std::set<std::string> 集合的读取代码。

//
// STL std::set<std::string> read
//
inline CArchive& AFXAPI operator >> (CArchive& ar, std::set<std::string>& val)
{
         int nSize;
         ar >> nSize;
         for (size_t i = 0; i < (size_t)nSize; i++)
         {
                 CStringA str;
                 ar >> str;
                 val.insert(std::string(str));
         }
         return ar;
}

序列化 STL 数据类型

STL 类型的序列化就像 SDK 数据结构的序列化一样简单。首先,我们需要一个提取和插入运算符的定义。要序列化或反序列化 std::string,我们需要添加以下运算符

//
// STL std::string write
//
 
inline CArchive& AFXAPI operator <<(CArchive& ar, const std::string& val)
{
         ar << CStringA(k.c_str()); // because std::string is ANSI we can pass it as a constructor to the CStringA class
         return ar;
}

反序列化 std::string

//
// STL std::string read
//
inline CArchive& AFXAPI operator >> (CArchive& ar, std::string& val)
{
         CStringA str;
         ar >> str;
         val = str;
         return ar;
}

关于 STL 数据和容器的序列化实现,我就讲到这里。当你看到一个 STL 集合和一个 STL 类型被序列化时,你就看到了全部。我将把序列化 std::pairstd::tuplestd::unordered_map 等作为练习留给读者。

序列化扁平 C 风格数组

要序列化扁平 C 数组,你将遵循与序列化集合相同的过程。但是因为扁平 C 风格数组的大小是已知的,所以没有必要在文件中存储其大小。

//
// Write float val[3]
//
inline CArchive& AFXAPI operator <<(CArchive& ar, float val[3])
{
         for(int i = 0; i < 3; i++)
         {
                 ar << val[i];
         }
         return ar;
}

读取扁平 C 风格数组。

//
// read float val[3]
//
inline CArchive& AFXAPI operator >> (CArchive& ar, float val[3])
{
         for (size_t i = 0; i < 3; i++)
         {
                 ar >> val[i];
         }
         return ar;
}

序列化枚举类型

要序列化枚举,你实际上只需要一个提取运算符,因为在插入时,枚举会隐式转换为 int。但是,为枚举同时提供插入和提取运算符会得到一个更清晰的解决方案,并可能消除未来讨厌的意外。

//
// Enumeration that we want to serialize
//
enum EMyTestEnum
{
         ENUM_0,
         ENUM_1,
};

写入枚举代码。

//
// Write enumeration EMyTestEnum
//
inline CArchive& AFXAPI operator <<(CArchive& ar, const EMyTestEnum& val)
{
         int iTemp = val;
         ar << iTemp;
         return ar;
}

读取枚举代码。

//
// Read enumeration EMyTestEnum
//
inline CArchive& AFXAPI operator >> (CArchive& ar, EMyTestEnum& val)
{
         int iTmp = 0;
         ar >> iTmp;
         val = (EMyTestEnum)iTmp;
         return ar;
}

CObject 派生类的序列化版本控制

这是一个相当有趣的话题,CObject 派生的版本控制可以通过两种方式完成。让我们假设我们有一个类,其版本随着核心应用程序中新功能的实现而不断演变。

//
// Any source code blocks look like this
//
class CMyObject : public CObject
{
         DECLARE_SERIAL(CMyObject)
public:
         CMyObject();
         virtual ~CMyObject();
         virtual void Serialize(CArchive& ar);
         
         // Version 1 data
         float m_f;
         double m_d;
 
         // Version 2 data
         COLORREF m_backColor;
         COLORREF m_foreColor;
 
         // Version 3 data
         CString m_strDescription;
         
         // Version 4 data
         CString m_strNotes;
};

要序列化这样一个对象,并且仍然能够读取版本 1、2 和 3 的旧文件,我们可以通过以下方式实现。

//
// Version 4 object
//
IMPLEMENT_SERIAL(CMyObject, CObject, VERSIONABLE_SCHEMA | 4)
 
void CMyObject::Serialize(CArchive& ar)
{
         if (ar.IsStoring())
         {        // storing code
                 ar << m_f;
                 ar << m_d;
                 ar << m_backColor;
                 ar << m_foreColor;
                 ar << m_strDescription;
                 ar << m_strNotes;
         }
         else
         {        // loading code
                 UINT nSchema = ar.GetObjectSchema();
                 switch (nSchema)
                 {
                 case 1:
                          ar >> m_f;
                          ar >> m_d;
                          break;
                 case 2:
                          ar >> m_f;
                          ar >> m_d;
                          ar >> m_backColor;
                          ar >> m_foreColor;
                          break;
                 case 3:
                          ar >> m_f;
                          ar >> m_d;
                          ar >> m_backColor;
                          ar >> m_foreColor;
                          ar >> m_strDescription;
                          break;
                 case 4:
                          ar >> m_f;
                          ar >> m_d;
                          ar >> m_backColor;
                          ar >> m_foreColor;
                          ar >> m_strDescription;
                          ar >> m_strNotes;
                          break;
                 }
         }
}

这种方法虽然清晰明了,但充其量是乏味的。有很多重复的代码。另一种方法是反向加载这些数据,让 switch case 语句穿透到正确版本的文件。

//
// Version 4 object
//
IMPLEMENT_SERIAL(CMyObject, CObject, VERSIONABLE_SCHEMA | 4)
 
void CMyObject::Serialize(CArchive& ar)
{
         if (ar.IsStoring())
         {        // storing code
 
                 // Add new features to the top rather than bottom
                 ar << m_strNotes;         // Version 4
                 ar << m_strDescription;   // Version 3
                 ar << m_backColor;        // Version 2
                 ar << m_foreColor;        // Version 2
                 ar << m_f;                // Version 1
                 ar << m_d;                // Version 1              
         }
         else
         {        // loading code
                 UINT nSchema = ar.GetObjectSchema();
                 switch (nSchema)
                 {
                 // Reverse case statements. New version goes on top without break statement 
                 // to let it simply fall through all the versions
                 case 4:
                          ar >> m_strNotes; // fall through to version 3
                 case 3:          
                          ar >> m_strDescription; // fall through to version 2
                 case 2:
                          ar >> m_backColor;
                          ar >> m_foreColor; // fall through to version 1
                 case 1:
                          ar >> m_f;
                          ar >> m_d;
                          break;    // finally break from version 1   
                 }
         }
}

这是一个更清晰的版本控制解决方案,消除了所有重复的代码。

非 CObject 类的序列化版本控制

要序列化非 CObject 派生类,我们只需遵循与 Windows SDK 结构相同的规则。

//
// Non CObject class
//
class CMyObject
{
public:
         CMyObject();
         virtual ~CMyObject();
 
         static const short VERSION = 1;
         
         float m_f;
         double m_d;
};

将版本号作为第一个成员写入。然后在读取时,根据文件内部的版本,你可以通过与加载的版本相对应的适当读取过程来处理它。

//
// Serializing CMyObject
//
 
// CMyObject write
inline CArchive& AFXAPI operator <<(CArchive& ar, const CMyObject & val)
{
         ar << val.VERSION; // Write the current version of the class as very first item
         ar << val.m_f;
         ar << val.m_d;
         return ar;
}
 
// CMyObject read
inline CArchive& AFXAPI operator >> (CArchive& ar, CMyObject & val)
{
         short nVersion = 0;
 
         ar >> nVersion;
         
         switch(nVersion)
         {
         case 1:
                  ar >> val.m_f;
                  ar >> val.m_d;
                 break;
         }
         return ar;
}

注意事项

永远不要序列化 WIN32 和 WIN64 typedefs!如果你将应用程序升级到 64 位,并尝试读取由 32 位版本应用程序创建的文件,而该文件恰好序列化了 WIN32/64 typedefs(例如 DWORD_PTR),那么它将惨败。因为 DWORD_PTR 在 32 位架构上是 4 字节长,在 WIN64 上是 8 字节长,所以将 4 字节读入 8 字节或反之,将导致 CArchiveException,并且它将使你的文件对于另一个位对齐版本的应用程序无用。只序列化硬性已知的类型。如果必须使用 64 位整数,则在 32 位和 64 位版本的应用程序中都明确地将其序列化为 __int64。如果你正在序列化 SDK 结构,这一点尤其需要关注。你需要仔细检查结构声明,如果可能存在 WIN32/64 typedefs,如果你正在构建 32 位应用程序并计划将来将其升级到 64 位,则明确地将它们转换为最大尺寸。

坚持使用 UNICODE 或 ANSI,句号。如果由于某种原因你必须同时维护应用程序的 ANSI 和 UNICODE 版本,那么请专门序列化 CStringACStringW,以便另一个版本可以读取该文件。可以说,像 "hello" 这样的字符串在 ANSI 字符串中将存储为 5 个字节长,但对于 UNICODE 版本则为 10 个字节长。

静态链接到 MFC 以消除对 MFCXX.DLL 或任何其他第三方库的运行时依赖。假设在第三方 DLL 的新版本中 sizeof(WhateverClass) 发生了变化,而你的应用程序动态链接到它并对其进行序列化,那么你的应用程序将无法读取文件。安全总比后悔好。所以如果你无法控制第三方库的代码,那么就静态链接到它。提前一点规划会大有裨益。

使用代码

我提供了 SerializeDemo 解决方案项目,该项目演示了本文中描述的所有方面。该解决方案包含 4 个子项目

  • SerializeData – 存放所有项目使用的数据结构和运算符
  • SerializeDemo – MFC 文档/视图应用程序
  • SerializeTcpServer – 一个在本地主机 "127.0.0.1" 端口 1011 上运行的控制台服务器应用程序。如果端口 1011 在你的机器上已被占用,你可能需要更改端口号。SerializeDemo 应用程序可以连接到此服务器以传输序列化数据
  • SerializationWithoutDocView – 演示在没有文档/视图架构的情况下使用 CArchive 的控制台应用程序

SerializeDemo 应用程序运行中。

尝试使用以下菜单命令来操作此应用程序。

尝试使用“编辑-复制”并粘贴到 SerializeDemo 应用程序的新实例中。

序列化 TCP 服务器运行中。

SerializeDemo 应用程序将序列化数据发送到服务器。服务器打印接收到的二进制数据。

历史

2017 年 3 月 16 日,原文。

2018 年 9 月 28 日。修正了一些拼写错误

2019 年 1 月 9 日。由于文章很长,添加了目录

© . All rights reserved.