類的可串列化

(filen (filen FileOpen

串列化(serialization)
??
作者:阿榮
串列化是微軟提供的用於對對象進行檔案I/O的一種機制,該機制在框架(Frame)/文檔(Document)/視圖(View) 模式中得到了很好的套用。
MFC 框架/文檔/視圖結構中的檔案讀寫
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<<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&#91;&#93; = __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: Ctypedptrlist m_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就具有串列化功能,其他圖形類可以類似定義。

相關詞條

熱門詞條

聯絡我們