面向对象有一个术语:Persistence,意思就是把对象永久保存下来。Power一关,啥都没有,对象如何能够永久保存呢?当然是写到文件中去了。把数据写到文件,很简单。在Document/CView结构中,数据都放在一份document里,我们只要把其中的成员变量依次写进文件即可。成员变量很肯能是一个对象,我们首先应该记载其类名称,然后才是对象中的数据。读取文档就有点麻烦了,当程序从文档中读到一个类名称时,它如何实例化(instance)一个对象?呵呵,这不就是动态创建的技术吗,我们在前面文章中已经解决掉了。 MFC有一套Serialize机制,目的在于把文件名的选择、文件的开关、缓冲区的建立、数据的读写、提取运算符>>和插入运算符<<的重载、对象的动态创建等都包装起来。上述Serialize的各部分工作,除了数据的读写和对象的动态创建外,其余都是枝节。动态创建的技术已经解决,让我们集中火力,分析数据的读写操作。
假设我有一份文件,用于记录一张图形,图形只有3种基本元素:线条(Stroke)、圆形、矩形。我打算用类来组织这份文件。
class CMyDoc : public CDocument { CObList m_graphList; CSize m_sizeDoc; //... }; //-------------------------------- class CStroke : public CObject { CDWordArray m_ptArray; //... }; //-------------------------------- class CRectangle : publc CObject { CRect m_rect; //... }; //-------------------------------- class CCirle : public CObject { CPoint m_center; UINT m_radius; //... };
其中COblist和CDwordArray是MFC提供的类,前者是一个链表,可放置任何从CObject派生下来的对象,后者是一个数组,每一个元素都是double word。另外3个类:CStroke、CRectangle、CCircle是我从CObject中派生下来的类。
假设有一份文件,文件中的数据为一个链表CObList m_graphList;其中链表中包含了100条线条、50个圆形、80个矩形、难道我们要记录230个类名称不成?当然不必,我们可以在每次记录对象内容的时候,先写入一个代码,表示此对象的类是否曾在文件中记录过了。如果是新类,乖乖的记录其类名称;如果是旧类,则以代码表示,这样可以节省文件大小以及程序用于解析的时间。啊,不要看到文件大小就想到硬盘很便宜,桌上的一切都将被带到网上,你得想想网络带宽这回事。
还有一个问题。文件的版本如何控制,旧版程序读取新版文件,新版程序读取旧版文件,都可能出问题,为了防弊,最好把版本号记录上去,最好是每个类都有自己的版本号码。
我希望有一个专门负责Serialization的函数,就叫做Serialize好了,假设现在我的Document类名称为CScribDoc,我希望有这么便利的程序方法:
void CScribDoc::Serialize(CArchive& ar) { if(ar.IsStoring()) ar<<m_sizeDoc; else ar>>m_sizeDoc; m_graphList.Serialize(ar); } //---------------------------------------- void CObList::Serialize(CArchive& ar) { if(ar.IsStoring()) { ar<<(WORD)m_nCount; for(CNode* pNode=m_pNodeHead;pNode!=NULL;pNode=pNode->pNext) ar<<pNode->data; } else { WORD nNewCount; ar>>nNewCount; while(nNewCount--) { CObject* newData; ar>>newData; AddTail(newData); } } } //------------------------------------------------------------------------------- void CStroke::Serialize(CArchive& ar) { m_ptArray.Serialize(ar); } //---------------------------------------------- void CDWordArray::Serialize(CArchive& ar) { if(ar.IsStoring()) { ar<<(WORD)m_nSize; for(int i=0;i<m_nSize;i++) ar<<m_pData[i]; } else { WORD nOldSize; ar>>nOldSize; for(int i=0;i<nOldSize;i++) ar>>m_pData[i]; } } //--------------------------------------------------------- void CRectangle::Serialize(CArchive& ar) { if(ar.IsStoring()) ar<<m_rect; else ar>>m_rect; } //--------------------------------------------------------- void CCircle::Serialize(CArchive& ar) { if(ar.IsStoring()) { ar<<(WORD)m_center.x; ar<<(WORD)m_center.y; ar<<(WORD)m_radius; } else { ar>>(WORD)m_center.x; ar>>(WORD)m_center.y; ar>>(WORD)m_radius; } }
每一个可写到文件或可从文件读取的类,都应该有自己的Serilize函数,负责把自己的数据读写文件操作。此类并且应该改写<<运算符和>>运算符。把数据导流到archive中,archive是什么?是一个与文件息息相关的缓冲区,暂时你可以想象他就是文件的化身。当CObList列表写入文件时,以此调用每个类的Serialize函数。
DECLARE_SERIAL/IMPLEMENT_SERIAL宏
要将<<和>>两个运算符重载,还要让Serialize函数神不知鬼不觉的放入类声明之中,最好的做法仍然是宏。类之所以能够进行文件读写操作,前提是拥有动态创建的能力,所以,MFC设计了两个宏DECLARE_SERIAL和IMPLEMENT_SERIAL:
#define DECLARE_SERIAL(class_name)/ DECLARE_DYNCREATE(class_name)/ friend CArchive& AFXAPI operator>>(CArchive& ar,class_name* &pOb); #define IMPLEMENT_SERIAL(class_name,base_class_name,wSchema)/ CObject* PASCAL class_name::CreateObject()/ {return new class_name;}/ _IMPLEMENT_RUNTIMECLASS(class_name,base_class_name,wSchema,/ class_mame::CreateObject)/ CArchive& AFXAPI operator>>(CArchive& ar,class_name* &pOb)/ {pOb=(class_name*)ar.ReadObject(RUNTIME_CLASS(class_name));/ return ar; }/
为了在每一个对象被处理(读或写)之前,能够处理琐屑的工作,诸如是否是第一次出现、记录版本号码、记录文件名等工作,CRuntimeClass需要2个函数Load和Store,这两个函数需要在CRuntimeClass结构体中增加。
为了让整个Serialization机制运行起来,我们必须做这样的类声明:
class CScribDoc : public CDocument { DECLARE_DYNCREATE(CScribDoc) }; class CStroke : public CObject { DECLARE_SERIAL(CStroke) public: void Serialize(CArchive&); }; class CRectangle : public CObject { DECLARE_SERIAL(CRectangle) public: void Serialize(CArchive&); }; class CCircle : public CObject { DECLARE_SERIAL(CCircle) public: void Serialize(CArchive&); }; //在.cpp文件中进行这样的操作: IMPLEMENT_DYNCREATE(CScribDoc,CDocument) IMPLEMENT_Serialize(CStroke,CObject,2) IMPLEMENT_Serialize(CRectangle,CObject,1) IMPLEMENT_Serialize(CCircle,CObject,1)
然后分头设计CStroke、CRectangle、CCircle的Serialize函数即可。