文章目录
MFC 实现了一套完整的序列化和反序列化机制,这套机制的核心是 CArchive 类。CArchive 类同时实现了序列化和反序列化的功能,由于反序列化是序列化的反向操作,故本文只关注序列化的实现。
CArchive 实现的是二进制序列化,现在来看这有些过时了(毕竟 MFC 太古老了)。比如在 .NET 框架里,虽然也支持二进制序列化,但主推的已经是 XML 序列化了。二进制序列化可读性差,不易反序列化。CArchive 序列化的成果,使用 CArchive 反序列化是很容易的,但要不使用 CArchive 或根本就不使用 C++ 语言进行反序列化,就不是那么简单的了。
CArchive 作为 MFC 序列化的核心,支持所有数据类型的序列化,包括基本数据类型、数组、字符串、MFC 类及派生类。下面逐一解析。
基本数据类型
基本数据类型使用 <<
运算符序列化,使用起来简单方便。都有哪些基本类型可以通过 <<
序列化,只要打开 MFC 源文件看看就知道了。
MFC 源文件通常位于类似这样的文件夹里:
C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\atlmfc
打开 ...\VC\atlmfc\include\afx.h
文件,在 class CArchive
的声明里可以找到下面的语句:
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);
CArchive& operator<<(bool b);
以上是一些基本数据类型,还有一些类也是使用 <<
运算符序列化,后面会讲到。
如果要看这些声明的实现代码,可以打开 ...\VC\atlmfc\include\afx.inl
文件,很容易就能找到,比如:
_AFX_INLINE CArchive& CArchive::operator<<(LONG l)
{
if(!IsStoring())
AfxThrowArchiveException(CArchiveException::readOnly,m_strFileName);
if (m_lpBufCur + sizeof(LONG) > m_lpBufMax) Flush();
*(UNALIGNED LONG*)m_lpBufCur = l; m_lpBufCur += sizeof(LONG); return *this;
}
_AFX_INLINE CArchive& CArchive::operator<<(DWORD dw)
{
if(!IsStoring())
AfxThrowArchiveException(CArchiveException::readOnly,m_strFileName);
if (m_lpBufCur + sizeof(DWORD) > m_lpBufMax) Flush();
*(UNALIGNED DWORD*)m_lpBufCur = dw; m_lpBufCur += sizeof(DWORD); return *this;
}
CPoint、CRect、CSize
这三个类也是使用 <<
运算符序列化,但在 CArchive
类里却找不到类似基本类型那样的运算符函数。
其实这三个类很特殊。这三个类不是纯正的 MFC 类,MFC 类通常都是继承自 CObject
,而这三个类分别继承自 POINT
、RECT
、SIZE
三个 Win32 结构。这三个类属于 MFC 和 ATL 共享的类。
这三个类的序列化,实际是通过它们的基类实现的。
打开 ...\VC\atlmfc\include\afxwin.h
文件,可以找到下面三个全局 <<
运算符函数,正是这三个函数实现了 CPoint
、CRect
、CSize
的序列化。
CArchive& AFXAPI operator<<(CArchive& ar, SIZE size);
CArchive& AFXAPI operator<<(CArchive& ar, POINT point);
CArchive& AFXAPI operator<<(CArchive& ar, const RECT& rect);
这三个函数的实现,可以在 ...\VC\atlmfc\include\afxwin1.inl
文件里找到。
_AFXWIN_INLINE CArchive& AFXAPI operator<<(CArchive& ar, SIZE size)
{ ar.Write(&size, sizeof(SIZE)); return ar; }
_AFXWIN_INLINE CArchive& AFXAPI operator<<(CArchive& ar, POINT point)
{ ar.Write(&point, sizeof(POINT)); return ar; }
_AFXWIN_INLINE CArchive& AFXAPI operator<<(CArchive& ar, const RECT& rect)
{ ar.Write(&rect, sizeof(RECT)); return ar; }
CString
CString
类也是使用 <<
运算符序列化,同样也找不到 CString
的 <<
运算符函数。
CString
与 CPoint、CRect、CSize 一样,也是 MFC 和 ATL 共享的类。
实际上 CString
是 CStringT
的别名。打开 ...\VC\atlmfc\include\afxstr.h
文件,在底部可以发现这样的语句:
typedef ATL::CStringT< wchar_t, StrTraitMFC_DLL< wchar_t > > CStringW;
typedef ATL::CStringT< char, StrTraitMFC_DLL< char > > CStringA;
typedef ATL::CStringT< TCHAR, StrTraitMFC_DLL< TCHAR > > CString;
而在 ...\VC\atlmfc\include\afx.h
文件里,在 class CArchive
的声明里可以找到这样的语句:
template< typename BaseType, class StringTraits >
CArchive& operator<<(const ATL::CStringT<BaseType, StringTraits>& str);
这个函数的实现,可以在 ...\VC\atlmfc\include\afx.inl
文件里找到:
template< typename BaseType, class StringTraits >
CArchive& CArchive::operator<<(const ATL::CStringT<BaseType, StringTraits>& str)
{
AfxWriteStringLength(*this, str.GetLength(), sizeof(BaseType) == sizeof(wchar_t));
Write(str, str.GetLength()*sizeof(BaseType));
return *this;
}
这应该就是 CString
类的序列化函数了。
CObject
CObject
是几乎所有 MFC 类的根(基类),CObject 提供了一个名为 Serialize
的虚函数,所有 CObject 的派生类都是通过重写这个虚函数实现序列化的。
virtual void Serialize(CArchive& ar);
上面这行语句可以在 ...\VC\atlmfc\include\afx.h
文件的 CObject
类声明里找到。
另外,在 ...\VC\atlmfc\include\afx.h
文件的 class CArchive
的声明里可以发现下面这样的语句:
// Object I/O is pointer based to avoid added construction overhead.
// Use the Serialize member function directly for embedded objects.
friend CArchive& AFXAPI operator<<(CArchive& ar, const CObject* pOb);
这个函数的实现可以在 ...\VC\atlmfc\include\afx.inl
文件里找到:
_AFX_INLINE CArchive& AFXAPI operator<<(CArchive& ar, const CObject* pOb)
{ ar.WriteObject(pOb); return ar; }
这说明,CObject
派生类还可以使用 <<
运算符序列化。CObArray::Serialize
函数里,就是使用的 <<
运算符。
要想使用 <<
运算符序列化 CObject
派生类对象,CObject
派生类必须添加 DECLARE_SERIAL
和 IMPLEMENT_SERIAL
宏。这在后面 定义可序列化的类 一节会讲到。
数组
CByteArray、CWordArray、CDWordArray、CUIntArray
这四个数组类型的共同特点是:1、都继承自 CObject
;2、存储的都是基本数据类型。
这四个数组类中,CByteArray
、CWordArray
、CDWordArray
都通过重写 CObject::Serialize
函数实现了序列化,但 CUIntArray
却没有重写这个函数,很奇怪。
这四个类的声明可以在 ...\VC\atlmfc\include\afxcoll.h
文件里找到,可见 CByteArray
、CWordArray
、CDWordArray
的类声明里都有下面这样的语句:
void Serialize(CArchive&);
而 CUIntArray
类的声明里却找不到这样的语句。
这四个类的实现代码可以分别在 ...\VC\atlmfc\src\mfc
文件夹里的 array_b.cpp
、array_w.cpp
、array_d.cpp
、array_u.cpp
文件里找到。
CStringArray
同样可以在 ...\VC\atlmfc\include\afxcoll.h
文件里找到 CStringArray
类的声明:
class CStringArray : public CObject
可见 CStringArray
继承自 CObject
类,类声明里可以发现有序列化函数:
void Serialize(CArchive&);
CStringArray
类的实现在 ...\VC\atlmfc\src\mfc\array_s.cpp
文件里。
void CStringArray::Serialize(CArchive& ar)
{
ASSERT_VALID(this);
CObject::Serialize(ar);
if (ar.IsStoring())
{
ar.WriteCount(m_nSize);
for (INT_PTR i = 0; i < m_nSize; i++)
ar << m_pData[i];
}
else
{
DWORD_PTR nOldSize = ar.ReadCount();
SetSize(nOldSize);
for (INT_PTR i = 0; i < m_nSize; i++)
ar >> m_pData[i];
}
}
CObArray、CPtrArray
这两个是指针数组,同样在 ...\VC\atlmfc\include\afxcoll.h
文件里声明。
CObArray
是 CObject*
指针数组,CPtrArray
是 void*
指针数组,这两个类都继承自 CObject
。
由类声明可见,只有 CObArray
重写了 CObject::Serialize
函数,CPtrArray
并没有实现序列化函数。其实这是可以理解的,CPtrArray
数组的成员是 void*
指针类型,也就是说,其数组成员实际是什么类型,是不清楚的,无法了解数据长度,所以也就没办法序列化。
这两个类的实现代码分别在 ...\VC\atlmfc\src\mfc
文件夹的 array_o.cpp
、array_p.cpp
文件里。
void CObArray::Serialize(CArchive& ar)
{
ASSERT_VALID(this);
CObject::Serialize(ar);
if (ar.IsStoring())
{
ar.WriteCount(m_nSize);
for (INT_PTR i = 0; i < m_nSize; i++)
ar << m_pData[i];
}
else
{
DWORD_PTR nOldSize = ar.ReadCount();
SetSize(nOldSize);
for (INT_PTR i = 0; i < m_nSize; i++)
ar >> m_pData[i];
}
}
注意这里使用 <<
运算符序列化对象,因此如果要使用 CObArray::Serialize
函数序列化,则类必须添加 DECLARE_SERIAL
和 IMPLEMENT_SERIAL
宏。
CArray
CArray
是模板数组,继承自 CObject
,在 ...\VC\atlmfc\include\afxtempl.h
文件里声明。
template<class TYPE, class ARG_TYPE = const TYPE&>
class CArray : public CObject
虽然 CArray 的标准用法是 CArray<TYPE, ARG_TYPE>
,但由于 ARG_TYPE 有缺省值 ARG_TYPE = const TYPE&
,所以一般来说,声明 CArray 对象时,只需指定 TYPE 就可以了,如 CArray<CPoint> ptArray;
。
CArray 重写了 CObject::Serialize
函数,CArray::Serialize
的实现也在 ...\VC\atlmfc\include\afxtempl.h
文件里。
template<class TYPE, class ARG_TYPE>
void CArray<TYPE, ARG_TYPE>::Serialize(CArchive& ar)
{
ASSERT_VALID(this);
CObject::Serialize(ar);
if (ar.IsStoring())
{
ar.WriteCount(m_nSize);
}
else
{
DWORD_PTR nOldSize = ar.ReadCount();
SetSize(nOldSize, -1);
}
SerializeElements<TYPE>(ar, m_pData, m_nSize);
}
template<class TYPE>
void AFXAPI SerializeElements(CArchive& ar, TYPE* pElements, INT_PTR nCount)
{
ENSURE(nCount == 0 || pElements != NULL);
ASSERT(nCount == 0 ||
AfxIsValidAddress(pElements, (size_t)nCount * sizeof(TYPE)));
// default is bit-wise read/write
if (ar.IsStoring())
{
TYPE* pData;
UINT_PTR nElementsLeft;
nElementsLeft = nCount;
pData = pElements;
while( nElementsLeft > 0 )
{
UINT nElementsToWrite;
nElementsToWrite = UINT(__min(nElementsLeft, INT_MAX/sizeof(TYPE)));
ar.Write(pData, nElementsToWrite*sizeof(TYPE));
nElementsLeft -= nElementsToWrite;
pData += nElementsToWrite;
}
}
else
{
TYPE* pData;
UINT_PTR nElementsLeft;
nElementsLeft = nCount;
pData = pElements;
while( nElementsLeft > 0 )
{
UINT nElementsToRead;
nElementsToRead = UINT(__min(nElementsLeft, INT_MAX/sizeof(TYPE)));
ar.EnsureRead(pData, nElementsToRead*sizeof(TYPE));
nElementsLeft -= nElementsToRead;
pData += nElementsToRead;
}
}
}
从 CArray::Serialize
的实现代码来看,如果 CArray
存储的是 CObject
的派生类,则似乎不适合使用 CArray::Serialize
序列化,而应该调用类自身的 Serialize
函数序列化。
CTypedPtrArray
CTypedPtrArray
也是模板数组,也在 ...\VC\atlmfc\include\afxtempl.h
文件里声明。
template<class BASE_CLASS, class TYPE>
class CTypedPtrArray : public BASE_CLASS
从类声明看,CTypedPtrArray
继承自 BASE_CLASS
。对于 BASE_CLASS
,微软文档说明:必须是数组类 CObArray
或 CPtrArray
。
微软文档对 CTypedPtrArray
的说明是:为 CPtrArray
类或 CObArray
类的对象提供类型安全的“包装器”。
我想可以这样理解,实际编码中,我们尽量不直接使用 CPtrArray
和 CObArray
,而是使用 CTypedPtrArray
。
CTypedPtrArray
本身没有重写 CObject::Serialize
函数,所以 CTypedPtrArray
的序列化是通过 BASE_CLASS
的 Serialize
函数实现的。
由于 CPtrArray
类没有 Serialize
函数,所以只有当 BASE_CLASS
为 CObArray
类时,CTypedPtrArray
才有序列化功能。
Lists 和 Maps
- Lists:
CPtrList
、CObList
、CStringList
、CList
、CTypedPtrList
- Maps:
CMapWordToOb
、CMapWordToPtr
、CMapPtrToWord
、CMapPtrToPtr
、CMapStringToPtr
、CMapStringToOb
、CMapStringToString
、CMap
、CTypedPtrMap
以上这些 List 类和 Map 类的声明和实现都在 ...\VC\atlmfc\include
文件夹的 afxcoll.h
和 afxtempl.h
文件里。
定义可序列化的类
定义我们自己的类时,如果想使用 MFC 的序列化机制,则需要按下面步骤来做:
- 从
CObject
类派生(或者从CObject
的派生类派生)。 - 重写
Serialize
成员函数。 - 在类声明中使用
DECLARE_SERIAL
宏。 - 定义没有参数的构造函数。
- 在类的实现中使用
IMPLEMENT_SERIAL
宏。
如果直接调用 Serialize
序列化,而不是通过 CArchive
的 >>
和 <<
运算符调用,则不需要最后三个步骤。
也就是说,DECLARE_SERIAL
和 IMPLEMENT_SERIAL
宏是为了使自定义类支持 <<
运算符序列化才需要添加的,如果自定义类不需要支持序列化,或者仅需要支持 Serialize
序列化,则不需要添加这两个宏。
比如,CObArray::Serialize
就是使用 <<
序列化的。如果自定义类要添加到 CObArray
或 CTypedPtrArray
中,并通过数组对象的 Serialize
函数序列化,那么自定义类就必须添加 DECLARE_SERIAL
和 IMPLEMENT_SERIAL
宏。
参见:序列化:定义可序列化的类
DECLARE_SERIAL
在 ...\VC\atlmfc\include\afx.h
文件里可找到 DECLARE_SERIAL
的定义:
#ifdef _AFXDLL
#define _DECLARE_DYNAMIC(class_name) \
protected: \
static CRuntimeClass* PASCAL _GetBaseClass(); \
public: \
static CRuntimeClass class##class_name; \
static CRuntimeClass* PASCAL GetThisClass(); \
virtual CRuntimeClass* GetRuntimeClass() const; \
#else
#define _DECLARE_DYNAMIC(class_name) \
public: \
static CRuntimeClass class##class_name; \
virtual CRuntimeClass* GetRuntimeClass() const; \
#endif
#define _DECLARE_DYNCREATE(class_name) \
_DECLARE_DYNAMIC(class_name) \
static CObject* PASCAL CreateObject();
#define DECLARE_SERIAL(class_name) \
_DECLARE_DYNCREATE(class_name) \
AFX_API friend CArchive& AFXAPI operator>>(CArchive& ar, class_name* &pOb);
IMPLEMENT_SERIAL
IMPLEMENT_SERIAL
宏也是在 ...\VC\atlmfc\include\afx.h
文件里定义的:
// Helper macros
#define _RUNTIME_CLASS(class_name) ((CRuntimeClass*)(&class_name::class##class_name))
#ifdef _AFXDLL
#define RUNTIME_CLASS(class_name) (class_name::GetThisClass())
#else
#define RUNTIME_CLASS(class_name) _RUNTIME_CLASS(class_name)
#endif
#ifdef _AFXDLL
#define _IMPLEMENT_RUNTIMECLASS(class_name, base_class_name, wSchema, pfnNew, class_init) \
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, pfnNew, \
&class_name::_GetBaseClass, NULL, class_init }; \
CRuntimeClass* PASCAL class_name::GetThisClass() \
{ return _RUNTIME_CLASS(class_name); } \
CRuntimeClass* class_name::GetRuntimeClass() const \
{ return _RUNTIME_CLASS(class_name); }
#else
#define _IMPLEMENT_RUNTIMECLASS(class_name, base_class_name, wSchema, pfnNew, class_init) \
AFX_COMDAT CRuntimeClass class_name::class##class_name = { \
#class_name, sizeof(class class_name), wSchema, pfnNew, \
RUNTIME_CLASS(base_class_name), NULL, class_init }; \
CRuntimeClass* class_name::GetRuntimeClass() const \
{ return RUNTIME_CLASS(class_name); }
#endif
#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; }
参阅:运行时对象模型服务
CArchive::WriteObject
CObject
的 <<
运算符函数内部就是调用 CArchive::WriteObject
函数序列化的。
所有 CObject
的派生类都可以使用 CArchive::WriteObject
函数序列化,但前提是 CObject
派生类必须添加 DECLARE_SERIAL
和 IMPLEMENT_SERIAL
宏。
CArchive::WriteObject
函数的实现可以在 ...\VC\atlmfc\src\mfc\arcobj.cpp
文件里找到:
void CArchive::WriteObject(const CObject* pOb)
{
// object can be NULL
ASSERT(IsStoring()); // proper direction
if (!IsStoring())
{
AfxThrowArchiveException(CArchiveException::readOnly, m_strFileName);
}
DWORD nObIndex;
ASSERT(sizeof(nObIndex) == 4);
ASSERT(sizeof(wNullTag) == 2);
ASSERT(sizeof(wBigObjectTag) == 2);
ASSERT(sizeof(wNewClassTag) == 2);
// make sure m_pStoreMap is initialized
MapObject(NULL);
if (pOb == NULL)
{
// save out null tag to represent NULL pointer
*this << wNullTag;
}
else if ((nObIndex = (DWORD)(DWORD_PTR)(*m_pStoreMap)[(void*)pOb]) != 0)
// assumes initialized to 0 map
{
// save out index of already stored object
if (nObIndex < wBigObjectTag)
*this << (WORD)nObIndex;
else
{
*this << wBigObjectTag;
*this << nObIndex;
}
}
else
{
// write class of object first
CRuntimeClass* pClassRef = pOb->GetRuntimeClass();
WriteClass(pClassRef);
// enter in stored object table, checking for overflow
CheckCount();
(*m_pStoreMap)[(void*)pOb] = (void*)(DWORD_PTR)m_nMapCount++;
// cause the object to serialize itself
((CObject*)pOb)->Serialize(*this);
}
}
在 CArchive::WriteObject
内部又调用了 CArchive::WriteClass
函数,最后调用对象自身的 Serialize
函数完成序列化。
void CArchive::WriteClass(const CRuntimeClass* pClassRef)
{
ASSERT(pClassRef != NULL);
ASSERT(IsStoring()); // proper direction
if (pClassRef == NULL)
{
AfxThrowArchiveException(CArchiveException::badClass, m_strFileName);
}
if (!IsStoring())
{
AfxThrowArchiveException(CArchiveException::genericException, m_strFileName);
}
if (pClassRef->m_wSchema == 0xFFFF)
{
TRACE(traceAppMsg, 0, "Warning: Cannot call WriteClass/WriteObject for %hs.\n",
pClassRef->m_lpszClassName);
AfxThrowNotSupportedException();
}
// make sure m_pStoreMap is initialized
MapObject(NULL);
// write out class id of pOb, with high bit set to indicate
// new object follows
// ASSUME: initialized to 0 map
DWORD nClassIndex;
if ((nClassIndex = (DWORD)(DWORD_PTR)(*m_pStoreMap)[(void*)pClassRef]) != 0)
{
// previously seen class, write out the index tagged by high bit
if (nClassIndex < wBigObjectTag)
*this << (WORD)(wClassTag | nClassIndex);
else
{
*this << wBigObjectTag;
*this << (dwBigClassTag | nClassIndex);
}
}
else
{
// store new class
*this << wNewClassTag;
pClassRef->Store(*this);
// store new class reference in map, checking for overflow
CheckCount();
(*m_pStoreMap)[(void*)pClassRef] = (void*)(DWORD_PTR)m_nMapCount++;
}
}
使用 CArchive::WriteObject
序列化对象,当同一个对象反复请求序列化时,为了避免重复存储相同的信息,WriteObject
对于同一个对象,只存储一次。这是如何实现的呢?
在 ...\VC\atlmfc\include\afx.h
文件的 class CArchive
的类声明里可以发现下面这样的语句:
// array/map for CObject* and CRuntimeClass* load/store
UINT m_nMapCount;
union
{
CPtrArray* m_pLoadArray;
CMapPtrToPtr* m_pStoreMap;
};
正是 m_pStoreMap
与 m_nMapCount
合作实现了这个机制。
在 CArchive::WriteObject
函数里,在调用被序列化对象的 Serialize
函数前,有这样一行语句:
(*m_pStoreMap)[(void*)pOb] = (void*)(DWORD_PTR)m_nMapCount++;
可见,每次序列化前,都把对象的指针和当前已存储对象的顺序号(索引号)存到了 m_pStoreMap
里。
那么这有什么用呢?以上代码是在一个 else
里,在 else
上面还有一个 else if
,如下:
else if ((nObIndex = (DWORD)(DWORD_PTR)(*m_pStoreMap)[(void*)pOb]) != 0)
// assumes initialized to 0 map
{
// save out index of already stored object
if (nObIndex < wBigObjectTag)
*this << (WORD)nObIndex;
else
{
*this << wBigObjectTag;
*this << nObIndex;
}
}
从这段代码可发现,在每次序列化前,都先从 m_pStoreMap
里获取该对象指针的索引号,如果索引号存在,说明该对象曾经序列化过,那么就只序列化该索引号,否则,序列化整个对象。
在 CArchive::WriteClass
函数里,也有类似的机制,可避免相同的类信息被反复存储。
(*m_pStoreMap)[(void*)pClassRef] = (void*)(DWORD_PTR)m_nMapCount++;
if ((nClassIndex = (DWORD)(DWORD_PTR)(*m_pStoreMap)[(void*)pClassRef]) != 0)
{
// previously seen class, write out the index tagged by high bit
if (nClassIndex < wBigObjectTag)
*this << (WORD)(wClassTag | nClassIndex);
else
{
*this << wBigObjectTag;
*this << (dwBigClassTag | nClassIndex);
}
}