作者:阿榮
下載例子源代碼
串行化是微軟提供的用于對(duì)對(duì)象進(jìn)行文件I/O的一種機(jī)制,該機(jī)制在框架(Frame)/文檔(Document)/視圖(View) 模式中得到了很好的應(yīng)用。很多人對(duì)什么是串行化、怎么使對(duì)象具有串行化能力和如何使用串行化功能等問(wèn)題都不甚明了。本文試圖對(duì)串行化做一個(gè)簡(jiǎn)單的解釋。由于本人對(duì)串行化功能使用的也不多,不足之處敬請(qǐng)諒解。

MFC 框架/文檔/視圖結(jié)構(gòu)中的文件讀寫
CFile是MFC類庫(kù)中所有文件類的基類。所有MFC提供的文件I/O功能都和這個(gè)類有關(guān)。很多情況下,大家都喜歡直接調(diào)用CFile::Write/WriteHuge來(lái)寫文件,調(diào)用CFile::Read/ReadHuge來(lái)讀文件。這樣的文件I/O其實(shí)和不使用MFC的文件 I/O沒有什么區(qū)別,甚至和以前的ANSI C的文件I/O也沒有多少差別,所差別的不外乎是調(diào)用的API不同而已。
在開始學(xué)習(xí)C++的時(shí)候,大家一定對(duì)cin/cout非常熟悉,這兩個(gè)對(duì)象使用非常明了的<<和>>運(yùn)算符進(jìn)行 I/O,其使用格式為:
//示例代碼1
int i;
cin >> i;
//here do something to object i
cout << i;
使用這種方式進(jìn)行I/O的好處時(shí),利用運(yùn)算符重載功能,可以用一個(gè)語(yǔ)句完成對(duì)一系列的對(duì)象的讀寫,而不需要區(qū)分對(duì)象具體的類型。MFC提供了類CArchive,實(shí)現(xiàn)了運(yùn)算符<<和>>的重載,希望按照前面cin和cout 的方式進(jìn)行文件I/O。通過(guò)和CFile類的配合,不僅僅實(shí)現(xiàn)了對(duì)簡(jiǎn)單類型如int/float等的文件讀寫,而且實(shí)現(xiàn)了對(duì)可序列化對(duì)象(Serializable Objects,這個(gè)概念后面描述)的文件讀寫。
一般情況下,使用CArchive對(duì)對(duì)象進(jìn)行讀操作的過(guò)程如下:
//示例代碼2
//定義文件對(duì)象和文件異常對(duì)象
CFile file;
CFileException fe;
//以讀方式打開文件
if(!file.Open(filename,CFile::modeRead,&fe))
{
fe.ReportError();
return;
}
//構(gòu)建CArchive 對(duì)象
CArchive ar(&file,CArchive::load);
ar >> obj1>>obj2>>obj3...>>objn;
ar.Flush();
//讀完畢,關(guān)閉文件流
ar.Close();
file.Close();
使用CArchive對(duì)對(duì)象進(jìn)行寫操作的過(guò)程如下:
//示例代碼3
//定義文件對(duì)象和文件異常對(duì)象
CFile file;
CFileException fe;
//以讀方式打開文件
if(!file.Open(filename,CFile::modeWrite|CFile::modeCreate,&fe))
{
fe.ReportError();
return;
}
//構(gòu)建CArchive 對(duì)象
CArchive ar(&file,CArchive::load);
ar << obj1<<obj2<<obj3...<<objn;
ar.Flush();
//寫完畢,關(guān)閉文件流
ar.Close();
file.Close();
可見,對(duì)于一個(gè)文件而言,如果文件內(nèi)對(duì)象的排列順序是固定的,那么對(duì)于文件讀和寫從形式上只有使用的運(yùn)算符的不同。在MFC的框架/文檔/視圖結(jié)構(gòu)中,一個(gè)文檔的內(nèi)部對(duì)象的構(gòu)成往往是固定的,這種情況下,寫到文件中時(shí)對(duì)象在文件中的布局也是固定的。因此CDocument利用其基類CObject提供的Serilize虛函數(shù),實(shí)現(xiàn)自動(dòng)文檔的讀寫。
當(dāng)用戶在界面上選擇文件菜單/打開文件(ID_FILE_OPEN)時(shí),CWinApp派生類的OnFileOpen函數(shù)被自動(dòng)調(diào)用,它通過(guò)文檔模板創(chuàng)建(MDI)/重用(SDI)框架、文檔和視圖對(duì)象,并最終調(diào)用CDocument::OnOpenDocument來(lái)讀文件,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;
}
同樣,當(dāng)用戶選擇菜單文件/文件保存(ID_FILE_SAVE)或者文件/另存為...(ID_FILE_SAVEAS)時(shí),通過(guò)CWinApp::OnFileSave和CWinApp::OnFileSaveAs 最終調(diào)用CDocument::OnSaveDocument,這個(gè)函數(shù)處理如下:
//示例代碼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
}
從前面兩段代碼可以看出,文件讀和文件寫的結(jié)構(gòu)基本相同,并且最終都調(diào)用了CObject::Serialize函數(shù)完成對(duì)文檔自己的讀和寫(參見注釋中的save me和load me)。對(duì)于用AppWizard自動(dòng)生成的MDI和SDI,系統(tǒng)自動(dòng)生成了這個(gè)函數(shù)的重載實(shí)現(xiàn),缺省的實(shí)現(xiàn)為:
//示例代碼6
void CMyDoc::Serialize(CArchive& ar)
{
if (ar.IsStoring())
{
// TODO: add storing code here
}
else
{
// TODO: add loading code here
}
}
如果一個(gè)對(duì)VC非常熟悉的人,喜歡手工生成所有的代碼(當(dāng)然這是非常浪費(fèi)時(shí)間也是沒有必要的),那么他提供的CDocument派生類也應(yīng)該實(shí)現(xiàn)這個(gè)缺省的Serialize函數(shù),否則,系統(tǒng)在文件讀寫時(shí)只能調(diào)用CObject::Serialize,這個(gè)函數(shù)什么都不做,當(dāng)然也無(wú)法完成對(duì)特定對(duì)象的文件保存/載入工作。當(dāng)然,用戶也可以截獲ID_FILE_OPEN等菜單,實(shí)現(xiàn)自己的文件讀寫功能,但是這樣的代碼將變得非常煩瑣,也不容易閱讀。
回到CMyDoc::Serialize函數(shù)。這個(gè)函數(shù)通過(guò)對(duì)ar對(duì)象的判斷,決定當(dāng)前是在讀還是在寫文件。由于AppWizard不知道你的文檔是干什么的,所以它不會(huì)給你添加實(shí)際的文件讀寫代碼。假設(shè)你的文檔中有三個(gè)對(duì)象m_Obj_a,m_Obj_b,m_Obj_c,那么實(shí)際的代碼應(yīng)該為:
//示例代碼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;
}
}
可串行化對(duì)象(Serializable Object)
要利用示例代碼7中的方式進(jìn)行文件I/O的一個(gè)基本條件是:m_Obj_a等對(duì)象必須是可串行化的對(duì)象。一個(gè)可串行化對(duì)象的條件為:
- 這個(gè)類從CObject派生)
- 該類實(shí)現(xiàn)了Serialize函數(shù)
- 該類在定義時(shí)使用了DECLARE_SERIAL宏
- 在類的實(shí)現(xiàn)文件中使用了IMPLEMENT_SERIAL宏
- 這個(gè)類有一個(gè)不帶參數(shù)的構(gòu)造函數(shù),或者某一個(gè)帶參數(shù)的構(gòu)造函數(shù)所有的參數(shù)都提供了缺省參數(shù)
這里,可串行化對(duì)象條件中沒有包括簡(jiǎn)單類型,對(duì)于簡(jiǎn)單類型,CArchive基本都實(shí)現(xiàn)了運(yùn)算符<<和>>的重載,所以可以直接使用串行化方式進(jìn)行讀寫。
從CObject類派生
串行化要求對(duì)象從CObject派生,或者從一個(gè)CObject的派生類派生。這個(gè)要求比較簡(jiǎn)單,因?yàn)閹缀跛械念悾ú话–String)都是從CObject 派生的,因此對(duì)于從MFC類繼承的類都滿足這個(gè)要求。對(duì)于自己的數(shù)據(jù)類,可以指定它的基類為CObject來(lái)滿足這個(gè)要求。
實(shí)現(xiàn)Serialize函數(shù)
Serialize函數(shù)是對(duì)象真正保存數(shù)據(jù)的函數(shù),是整個(gè)串行化的核心。其實(shí)現(xiàn)方法和CMyDoc::Serialize一樣,利用CArchive::IsStoring和CArchive::IsLoading 判斷當(dāng)前的操作,并選擇<<和>>來(lái)保存和讀取對(duì)象。
使用DECLARE_SERIAL宏
DECLARE_SERIAL宏包括了DECLARE_DYNAMIC和DECLARE_DYNCREATE功能,它定義了一個(gè)類的CRuntimeClass相關(guān)信息,并實(shí)現(xiàn)了缺省的operator >> 重載。實(shí)現(xiàn)了該宏以后,CArchive就可以利用ReadObject和WriteObject來(lái)進(jìn)行對(duì)象I/O,并能夠在事先不知道類型的情況下從文件中讀對(duì)象。
使用IMPLEMENT_SERIAL
DECLARE_SERIAL宏和IMPLEMENT_SERIAL宏必須成對(duì)出現(xiàn),否則DECLARE_SERIAL宏定義的實(shí)體將無(wú)法實(shí)現(xiàn),最終導(dǎo)致連接錯(cuò)誤。
缺省構(gòu)造函數(shù)
這是CRuntimeClass::CreateObject對(duì)對(duì)象的要求。
特殊情況
- 只通過(guò)Serialize函數(shù)對(duì)對(duì)象讀寫,而不使用ReadObject/WriteObject和運(yùn)算符重載時(shí),前面的可串行化條件不需要,只要實(shí)現(xiàn)Serialize 函數(shù)即可。
- 對(duì)于現(xiàn)存的類,如果它沒有提供串行化功能,可以通過(guò)使用重載友元operator <<和operator >>來(lái)實(shí)現(xiàn)。
例子
假設(shè)需要實(shí)現(xiàn)一個(gè)幾何圖形顯示、編輯程序,支持可擴(kuò)展的圖形功能。這里不想討論具體圖形系統(tǒng)的實(shí)現(xiàn),只討論圖像對(duì)象的保存和載入。
基類CPicture
每個(gè)圖形對(duì)象都從CPicture派生,這個(gè)類實(shí)現(xiàn)了串行化功能,其實(shí)現(xiàn)代碼為:
//頭文件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)
{
//基類不實(shí)現(xiàn)繪圖功能,由派生類實(shí)現(xiàn)
}
void CPicture::Serialize(CArchive & ar)
{
if(ar.IsLoading())
{
ar << m_nType;
}else{
ar >> m_nType;
}
}
注意:由于CRuntimeClass要求這個(gè)對(duì)象必須能夠被實(shí)例化,因此雖然Draw函數(shù)沒有任何繪圖操作,這個(gè)類還是沒有把它定義成純虛函數(shù)。
對(duì)象在CDocument派生類中的保存和文件I/O過(guò)程
為了簡(jiǎn)化設(shè)計(jì),在CDocument類派生類中,采用MFC提供的模板類CPtrList來(lái)保存對(duì)象。該對(duì)象定義為:
protected:
CTypedPtrList m_listPictures;
由于CTypedPtrList和CPtrList都沒有實(shí)現(xiàn)Serialize函數(shù),因此不能夠通過(guò)ar << m_listPictures和ar >> m_listPictures 來(lái)序列化對(duì)象,因此CPictureDoc的Serialize函數(shù)需要如下實(shí)現(xiàn):
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);
}
實(shí)現(xiàn)派生類的串行化功能
幾何圖形程序支持直線、矩形、三角形、橢圓等圖形,分別以類CLine、CRectangle、CTriangle和CEllipse實(shí)現(xiàn)。以類CLine為例,實(shí)現(xiàn)串行化功能:
- 從CPicture派生CLine,在CLine類定義中增加如下成員變量:
CPoint m_ptStart,m_ptEnd;
- 在該行下一行增加如下宏:
DECLARE_SERIAL(CLine)
- 實(shí)現(xiàn)Serialize函數(shù)
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就具有串行化功能,其他圖形類可以類似定義。
附注
本文倉(cāng)促草就,不足之處在所難免。請(qǐng)發(fā)現(xiàn)謬誤者給我來(lái)信說(shuō)明,謝謝。
from: http://www.vckbase.com/document/viewdoc/?id=918