分类:
2008-10-14 14:50:09
串行化(Serialization)
作者:
串行化是微软提供的用于对对象进行文件I/O的一种机制,该机制在框架(Frame)/文档(Document)/视图(View)
模式中得到了很好的应用。很多人对什么是串行化、怎么使对象具有串行化能力和如何使用串行化功能等问题都
不甚明了。本文试图对串行化做一个简单的解释。由于本人对串行化功能使用的也不多,不足之处敬请谅解。
CFile是MFC类库中所有文件类的基类。所有MFC提供的文件I/O功能都和这个类有关。很多情况下,大家都喜欢直接 调用CFile::Write/WriteHuge来写文件,调用CFile::Read/ReadHuge来读文件。这样的文件I/O其实和不使用MFC的文件 I/O没有什么区别,甚至和以前的ANSI C的文件I/O也没有多少差别,所差别的不外乎是调用的API不同而已。
在开始学习C++的时候,大家一定对cin/cout非常熟悉,这两个对象使用非常明了的<<和>>运算符进行
I/O,其使用格式为:
//示例代码1 int i; cin >> i; //here do something to object i cout << i;
使用这种方式进行I/O的好处时,利用运算符重载功能,可以用一个语句完成对一系列的对象的读写,而不需要 区分对象具体的类型。MFC提供了类CArchive,实现了运算符<<和>>的重载,希望按照前面cin和cout 的方式进行文件I/O。通过和CFile类的配合,不仅仅实现了对简单类型如int/float等的文件读写,而且实现了对可 序列化对象(Serializable Objects,这个概念后面描述)的文件读写。
一般情况下,使用CArchive对对象进行读操作的过程如下:
//示例代码2 //定义文件对象和文件异常对象 CFile file; CFileException fe; //以读方式打开文件 if(!file.Open(filename,CFile::modeRead,&fe)) { fe.ReportError(); return; } //构建CArchive 对象 CArchive ar(&file,CArchive::load); ar >> obj1>>obj2>>obj3...>>objn; ar.Flush(); //读完毕,关闭文件流 ar.Close(); file.Close();
使用CArchive对对象进行写操作的过程如下:
//示例代码3 //定义文件对象和文件异常对象 CFile file; CFileException fe; //以读方式打开文件 if(!file.Open(filename,CFile::modeWrite|CFile::modeCreate,&fe)) { fe.ReportError(); return; } //构建CArchive 对象 CArchive ar(&file,CArchive::load); ar << obj1<可见,对于一个文件而言,如果文件内对象的排列顺序是固定的,那么对于文件读和写从形式上只有使用的运算符的不同。 在MFC的框架/文档/视图结构中,一个文档的内部对象的构成往往是固定的,这种情况下,写到文件中时对象在文件中的布局 也是固定的。因此CDocument利用其基类CObject提供的Serilize虚函数,实现自动文档的读写。
当用户在界面上选择文件菜单/打开文件(ID_FILE_OPEN)时,CWinApp派生类的OnFileOpen函数被自动调用,它通过文档模板 创建(MDI)/重用(SDI)框架、文档和视图对象,并最终调用CDocument::OnOpenDocument来读文件,CDocument::OnOpenDocument 的处理流程如下:
//示例代码4 BOOL CDocument::OnOpenDocument(LPCTSTR lpszPathName) { if (IsModified()) TRACE0("Warning: OnOpenDocument replaces an unsaved document.\n"); CFileException fe; CFile* pFile = GetFile(lpszPathName, CFile::modeRead|CFile::shareDenyWrite, &fe); if (pFile == NULL) { ReportSaveLoadException(lpszPathName, &fe, FALSE, AFX_IDP_FAILED_TO_OPEN_DOC); return FALSE; } DeleteContents(); SetModifiedFlag(); // dirty during de-serialize CArchive loadArchive(pFile, CArchive::load | CArchive::bNoFlushOnDelete); loadArchive.m_pDocument = this; loadArchive.m_bForceFlat = FALSE; TRY { CWaitCursor wait; if (pFile->GetLength() != 0) Serialize(loadArchive); // load me loadArchive.Close(); ReleaseFile(pFile, FALSE); } CATCH_ALL(e) { ReleaseFile(pFile, TRUE); DeleteContents(); // remove failed contents TRY { ReportSaveLoadException(lpszPathName, e, FALSE, AFX_IDP_FAILED_TO_OPEN_DOC); } END_TRY DELETE_EXCEPTION(e); return FALSE; } END_CATCH_ALL SetModifiedFlag(FALSE); // start off with unmodified return TRUE; }同样,当用户选择菜单文件/文件保存(ID_FILE_SAVE)或者文件/另存为...(ID_FILE_SAVEAS)时,通过CWinApp::OnFileSave和CWinApp::OnFileSaveAs 最终调用CDocument::OnSaveDocument,这个函数处理如下:
//示例代码5 BOOL CDocument::OnSaveDocument(LPCTSTR lpszPathName) { CFileException fe; CFile* pFile = NULL; pFile = GetFile(lpszPathName, CFile::modeCreate | CFile::modeReadWrite | CFile::shareExclusive, &fe); if (pFile == NULL) { ReportSaveLoadException(lpszPathName, &fe, TRUE, AFX_IDP_INVALID_FILENAME); return FALSE; } CArchive saveArchive(pFile, CArchive::store | CArchive::bNoFlushOnDelete); saveArchive.m_pDocument = this; saveArchive.m_bForceFlat = FALSE; TRY { CWaitCursor wait; Serialize(saveArchive); // save me saveArchive.Close(); ReleaseFile(pFile, FALSE); } CATCH_ALL(e) { ReleaseFile(pFile, TRUE); TRY { ReportSaveLoadException(lpszPathName, e, TRUE, AFX_IDP_FAILED_TO_SAVE_DOC); } END_TRY DELETE_EXCEPTION(e); return FALSE; } END_CATCH_ALL SetModifiedFlag(FALSE); // back to unmodified return TRUE; // success }从前面两段代码可以看出,文件读和文件写的结构基本相同,并且最终都调用了CObject::Serialize函数完成对文档自己的读和写(参见 注释中的save me和load me)。对于用AppWizard自动生成的MDI和SDI,系统自动生成了这个函数的重载实现,缺省的实现为:
//示例代码6 void CMyDoc::Serialize(CArchive& ar) { if (ar.IsStoring()) { // TODO: add storing code here } else { // TODO: add loading code here } }如果一个对VC非常熟悉的人,喜欢手工生成所有的代码(当然这是非常浪费时间也是没有必要的),那么他提供的CDocument派生类 也应该实现这个缺省的Serialize函数,否则,系统在文件读写时只能调用CObject::Serialize,这个函数什么都不做,当然也无法完成 对特定对象的文件保存/载入工作。当然,用户也可以截获ID_FILE_OPEN等菜单,实现自己的文件读写功能,但是这样的代码将变得非常烦琐, 也不容易阅读。
回到CMyDoc::Serialize函数。这个函数通过对ar对象的判断,决定当前是在读还是在写文件。由于AppWizard不知道你的文档是干 什么的,所以它不会给你添加实际的文件读写代码。假设你的文档中有三个对象m_Obj_a,m_Obj_b,m_Obj_c,那么实际的代码应该为:
//示例代码7 void CMyDoc::Serialize(CArchive& ar) { if (ar.IsStoring()) { ar << m_Obj_a << m_Obj_b << m_Obj_c; } else { ar >> m_Obj_a >> m_Obj_b >> m_Obj_c; } }可串行化对象(Serializable Object)
要利用中的方式进行文件I/O的一个基本条件是:m_Obj_a等对象必须是可串行化的对象。一个 可串行化对象的条件为:
- 这个类从CObject派生)
- 该类实现了Serialize函数
- 该类在定义时使用了DECLARE_SERIAL宏
- 在类的实现文件中使用了IMPLEMENT_SERIAL宏
- 这个类有一个不带参数的构造函数,或者某一个带参数的构造函数所有的参数都提供了缺省参数
这里,可串行化对象条件中没有包括简单类型,对于简单类型,CArchive基本都实现了运算符<<和>>的重载,所以可以直接使用 串行化方式进行读写。
从CObject类派生
串行化要求对象从CObject派生,或者从一个CObject的派生类派生。这个要求比较简单,因为几乎所有的类(不包括CString)都是从CObject 派生的,因此对于从MFC类继承的类都满足这个要求。对于自己的数据类,可以指定它的基类为CObject来满足这个要求。
实现Serialize函数
Serialize函数是对象真正保存数据的函数,是整个串行化的核心。其实现方法和CMyDoc::Serialize一样,利用CArchive::IsStoring和CArchive::IsLoading 判断当前的操作,并选择<<和>>来保存和读取对象。
使用DECLARE_SERIAL宏
DECLARE_SERIAL宏包括了DECLARE_DYNAMIC和DECLARE_DYNCREATE功能,它定义了一个类的CRuntimeClass相关信息,并实现了缺省的operator >> 重载。实现了该宏以后,CArchive就可以利用ReadObject和WriteObject来进行对象I/O,并能够在事先不知道类型的情况下从文件中读对象。
使用IMPLEMENT_SERIAL
DECLARE_SERIAL宏和IMPLEMENT_SERIAL宏必须成对出现,否则DECLARE_SERIAL宏定义的实体将无法实现,最终导致连接错误。
缺省构造函数
这是CRuntimeClass::CreateObject对对象的要求。
特殊情况
- 只通过Serialize函数对对象读写,而不使用ReadObject/WriteObject和运算符重载时,前面的可串行化条件不需要,只要实现Serialize 函数即可。
- 对于现存的类,如果它没有提供串行化功能,可以通过使用重载友元operator <<和operator >>来实现。
例子
假设需要实现一个几何图形显示、编辑程序,支持可扩展的图形功能。这里不想讨论具体图形系统的实现,只讨论图像对象的保存和 载入。
基类CPicture
每个图形对象都从CPicture派生,这个类实现了串行化功能,其实现代码为:
//头文件picture.h #if !defined(__PICTURE_H__) #define __PICTURE_H__ #if _MSC_VER > 1000 #pragma once #endif // _MSC_VER > 1000 const int TYPE_UNKNOWN = -1; class CPicture:public CObject { int m_nType;//图形类别 DECLARE_SERIAL(CPicture) public: CPicture(int m_nType=TYPE_UNKNOWN):m_nType(m_nType){}; int GetType()const {return m_nType;}; virtual void Draw(CDC * pDC); void Serialize(CArchive & ar); }; #endif //cpp文件picture.cpp #include "stdafx.h" #include "picture.h" #ifdef _DEBUG #define new DEBUG_NEW #undef THIS_FILE static char THIS_FILE[] = __FILE__; #endif void CPicture::Draw(CDC * pDC) { //基类不实现绘图功能,由派生类实现 } void CPicture::Serialize(CArchive & ar) { if(ar.IsLoading()) { ar << m_nType; }else{ ar >> m_nType; } }注意:由于CRuntimeClass要求这个对象必须能够被实例化,因此虽然Draw函数没有任何绘图操作,这个类还是没有把它定义成 纯虚函数。
对象在CDocument派生类中的保存和文件I/O过程
为了简化设计,在CDocument类派生类中,采用MFC提供的模板类CPtrList来保存对象。该对象定义为:
protected: CTypedPtrListm_listPictures; 由于CTypedPtrList和CPtrList都没有实现Serialize函数,因此不能够通过ar << m_listPictures和ar >> m_listPictures 来序列化对象,因此CPictureDoc的Serialize函数需要如下实现:
void CTsDoc::Serialize(CArchive& ar) { POSITION pos; if (ar.IsStoring()) { // TODO: add storing code here pos = m_listPictures.GetHeadPosition(); while(pos != NULL) { ar << m_listPictures.GetNext (pos); } } else { // TODO: add loading code here RemoveAll(); CPicture * pPicture; do{ try { ar >> pPicture; TRACE("Read Object %d\n",pPicture->GetType ()); m_listPictures.AddTail(pPicture); } catch(CException * e) { e->Delete (); break; } }while(pPicture != NULL); } m_pCurrent = NULL; SetModifiedFlag(FALSE); }实现派生类的串行化功能
几何图形程序支持直线、矩形、三角形、椭圆等图形,分别以类CLine、CRectangle、CTriangle和CEllipse实现。以类CLine为例,实现 串行化功能:
- 从CPicture派生CLine,在CLine类定义中增加如下成员变量:
CPoint m_ptStart,m_ptEnd;- 在该行下一行增加如下宏:
DECLARE_SERIAL(CLine)- 实现Serialize函数
void CLine::Serialize(CArchive & ar) { CPicture::Serialize(ar); if(ar.IsLoading()) { ar>>m_ptStart.x>>m_ptStart.y>>m_ptEnd.x>>m_ptEnd.y; }else{ ar<- 在CPP文件中增加
IMPLEMENT_SERIAL(CLine,CPicture,TYPE_LINE);这样定义的CLine就具有串行化功能,其他图形类可以类似定义。
附注
本文仓促草就,不足之处在所难免。请发现谬误者给来信说明,谢谢。
--------------------next---------------------
罗恩大哥,你好,我在实际变成中想自动载入上次的文件有什么好的方法吗?? ( yangqifengfan 发表于 2005-3-25 8:24:00)
写的不错。。。。 ( 徐明刚 发表于 2004-10-12 12:27:00)
罗恩大哥,你好,我是vc++初学者,(以前看过windows程序设计(第5版),
机工社的《c++精髓 软件工程方法》),
最近在看《深入浅出MFC》,第8章时,里边也说到了Serialize,我不是很懂,
我看你的个人专栏里边看了串行化(Serialization)这篇文章也提到了,
你能顺便给我解释一下吗Serialize
在你的示例代码7中有 ar >> m_Obj_a
这里operator >>是不是调用
_AFX_INLINE CArchive& AFXAPI operator>>(CArchive& ar,CObject*& pOb) ?
如果是,那么IMPLEMENT_SERIAL宏 展开后的
CArchive& AFXAPI operator>>(CArchive& ar, class_name* &pOb)
在哪里用到?
还有为什么IMPLEMENT_SERIAL宏 展开后的
有CArchive& AFXAPI operator>>(CArchive& ar, class_name* &pOb)
没有CArchive& AFXAPI operator<<(CArchive& ar, class_name* &pOb)
在示例7中出现ar << m_Obj_a ( yckyck2001 发表于 2004-7-11 14:03:00)
我就把你当成我现在的目标了 ( 弃卒 发表于 2004-6-14 15:25:00)
将内存指针系列化可不是个好主意(m_listPictures这一段) ( jiangsanhuo 发表于 2004-2-4 9:04:00)
最明显的错误是:
ar >> pPicture;之前没有为pPicture分配内存: pPicture = new CPicture; ( eeixy2000 发表于 2003-12-27 19:12:00)
唉,希望这个文档不要被大家看成是教人家怎么绘图的:( ( 阿荣 发表于 2003-11-20 13:51:00)
建议看看vckbase上面得EastDraw程序,该文章中说得所有内容都可以从那个例子中找到,并且那个例子中还有undo功能得实现。 ( coyer 发表于 2003-11-6 17:14:00)
不错,建议以后多发表类似的文章! ( xiaojin 发表于 2003-11-4 23:06:00)
如果发生版本升级,那么新版本的程序能不能兼容旧版本本来就需要看代码怎么设计,这种情况下,不使用串行化一样不能达到目的。有两个方法可以解决这个问题:1)根据扩展名不同实现不同的Serialize功能,可以在Serialize中判断文件扩展名,调用不同子函数实现。2)使用版本升级工具,在处理之前先转换文件格式 ( 阿荣 发表于 2003-11-4 13:29:00)
.......................................................
--------------------next---------------------