串行化(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();
//示例代碼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<<obj2<<obj3...<<objn; ar.Flush(); //寫完畢,關閉文件流 ar.Close(); file.Close();
可見,對于一個文件而言,如果文件內對象的排列順序是固定的,那么對于文件讀和寫從形式上只有使用的運算符的不同。在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)
要利用示例代碼7中的方式進行文件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<<m_ptStart.x<<m_ptStart.y<<m_ptEnd.x<<m_ptEnd.y; } }
- 在CPP文件中增加
IMPLEMENT_SERIAL(CLine,CPicture,TYPE_LINE);
這樣定義的CLine就具有串行化功能,其他圖形類可以類似定義。
附注
本文倉促草就,不足之處在所難免。請發現謬誤者給我來信說明,謝謝。
from: http://www.vckbase.com/document/viewdoc/?id=918
posted on 2006-08-13 11:12 weidagang2046 閱讀(212) 評論(0) 編輯 收藏 所屬分類: Windows