Visual C++中對象的序列化與文件I/O研究
發表于:2007-07-14來源:作者:點擊數:
標簽:
持久性和序列化 持久性是對象所有的保存和加載其狀態數據的能力。具有這種能力的對象能夠在應用程序結束之前以某種方式將當前的對象狀態數據記錄下來,當程序再次運行時,通過對這些數據的讀取而恢復到上一次任務結束時的狀態。由于絕大多數的MFC類是直接或
持久性和序列化
持久性是對象所有的保存和加載其狀態數據的能力。具有這種能力的對象能夠在應用程序結束之前以某種方式將當前的對象狀態數據記錄下來,當程序再次運行時,通過對這些數據的讀取而恢復到上一次任務結束時的狀態。由于絕大多數的MFC類是直接或間接由MFC的CObject類派生出來的,因此這些MFC類都具有保存和加載對象狀態的能力,是具有持久性的。在使用應用程序向導生成文檔/視結構的程序框架時,就已經為應用程序提供了用于對象狀態數據保存和加載的基本代碼。
為實現對象的持久性,通常多以字節流的形式將記錄對象狀態的數據存放到磁盤上,這種將狀態數據保存到磁盤和從磁盤恢復到內存的過程稱為序列化。序列化是MFC的一個重要概念,是MFC文檔/視圖結構應用程序能進行文檔打開、保存等操作的基礎。當在MFC框架程序中裝載或保存一個文件時,除了打開文件以供程序讀寫外,還傳遞給應用程序一個相關的CArchive對象,并以此實現對持久性數據的序列化。
大多數MFC應用程序在實現對象的持久性時并非直接用MFC的CFile類對磁盤文件進行讀寫(有關CFile類的詳細介紹將在下一節進行),而是通過使用CArchive對象并由其對CFile成員函數進行調用來執行文件I/O操作。CArchive類支持復合對象的連續二進制形式的輸入輸出。在構造一個CArchive對象或將其連接到一個表示打開的文件的CFile對象后,可以指定一個檔案是被裝載還是被保存。MFC允許使用操作符“<<”和“>>”來對多種原始數據類型進行序列化。這些原始數據類型包括BYTE,WORD,LONG,DWORD,float,double,int,unsigned int,short和char等。
對于其他由MFC類來表示的非原始數據類型如CString對象等的序列化則可以通過對“<<”和“>>”運算符的重載來解決,可以用此方式進行序列化的MFC類和結構有CString,CTime,CTimeSpan,COleVari
ant,COleCurreny,COleDateTime,COleDateTimeSpan,CSize,CPoint,CRect,SIZE,POINT和RECT等。除了操作符“<<”和“>>”之外還可以調用CArchive類成員函數Read()和Write()來完成序列化。下面這段代碼展示了通過操作符對int型變量VarA、VarB的序列化過程:
// 將VarA、VarB存儲到檔案中
CArchive ar (&file, CArchive::store);
ar << VarA << VarB;
……
// 從檔案裝載VarA、VarB
CArchive ar (&file, CArchive::load)
ar >> VarA >> VarB;
CArchive類僅包含有一個數據成員m__pDocument。在執行菜單上的打開或保存命令時,程序框架將會把該數據成員設置為要被序列化的文檔。另外需要特別注意的是:在使用CArchive類時,要保證對CArchive對象的操作與文件訪問權限的統一。
在本文下面將要給出的示例程序中,將對繪制連線所需要的關鍵點坐標和坐標個數等持久性對象進行序列化。其中文檔類的成員變量m_nCount和m_ptPosition[100]分別記錄了當前點的個數和坐標,初始值為0。當鼠標點擊客戶區時將對點的個數進行累加,并保存當前點的坐標位置。隨后通過Invalidate()函數發出WM_PAINT消息通知窗口對客戶區進行重繪,在重繪代碼中對這些點擊過的點進行繪圖連線:
void CSample04View::OnLButtonDown(UINT nFlags, CPoint point)
{
// 獲取指向文檔類的指針
CSample04Doc* pDoc = GetDocument();
// 保存當前鼠標點擊位置
pDoc->m_ptPosition[pDoc->m_nCount] = point;
if (pDoc->m_nCount < 100)
pDoc->m_nCount++;
// 刷新屏幕
Invalidate();
CView::OnLButtonDown(nFlags, point);
}
……
void CSample04View::OnDraw(CDC* pDC)
{
CSample04Doc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
// 對點擊的點進行連線繪圖
pDC->MoveTo(pDoc->m_ptPosition[0]);
for (int i = 1; i < pDoc->m_nCount; i++)
pDC->LineTo(pDoc->m_ptPosition[i]);
}
從上述程序代碼不難看出,為了能保存繪制結果需要對文檔類的成員變量m_nCount和m_ptPosition[100]進行序列化處理。而文檔類成員函數Serialize()則通過Archive類為這些持久性對象的序列化提供了功能上的支持。下面的代碼完成了對持久性對象的保存和加載:
if (ar.IsStoring())
{
// 存儲持久性對象到檔案
ar << m_nCount;
for (int i = 0; i < m_nCount; i++)
ar << m_ptPosition[i];
}
else
{
// 從檔案裝載持久性對象
ar >> m_nCount;
for (int i = 0; i < m_nCount; i++)
ar >> m_ptPosition[i];
}
自定義持久類
為了使一個類的對象成為持久的,可以自定義一個持久類,將持久性數據的存儲和加載的工作交由自定義類自己去完成。這種處理方式也更加符合
面向對象的程序設計要求??梢酝ㄟ^下面幾個基本步驟來創建一個能序列化其成員變量的自定義持久類:
1. 直接或間接從CObject類派生出一個新類。
2. 在類的聲明部分包含MFC的DECLARE_SERIAL宏,該宏只需要將類名作為參數。
3. 重載基類的Serialize()函數,并添加對數據成員進行序列化的代碼。
4. 如果構造函數沒有一個空的缺省的構造函數(不含任何參數),為其添加一個。
5. 在類的實現部分,添加MFC的IMPLEMENT_SERIAL宏。該宏需要三個參數:類名,基類名和一個方案號。其中方案號是一個相當于版本號的整數,每當改變了類的序列化數據格式后就應當及時更改此數值。
根據上述步驟不難對上一小節中的序列化代碼進行封裝,封裝后的持久類CPosition負責對類成員變量m_nCount和m_ptPosition[100]的序列化,封裝后的代碼如下:
// CPosition類聲明部分:
class CPosition : public CObject
{
DECLARE_SERIAL(CPosition)
CPosition();
int m_nCount;
CPoint m_ptPosition[100];
void Serialize(CArchive& ar);
CPoint GetValue(int index);
void SetValue(int index, CPoint point);
virtual ~CPosition();
};
……
// CPosition類實現部分:
IMPLEMENT_SERIAL(CPosition, CObject, 0)
CPosition::CPosition()
{
// 對類成員進行初始化
m_nCount = 0;
for (int i = 0; i < 100; i++)
m_ptPosition[i] = CPoint (0, 0);
}
CPosition::~CPosition()
{
}
void CPosition::SetValue(int index, CPoint point)
{
// 設置指定點的坐標值
m_ptPosition[index] = point;
}
CPoint CPosition::GetValue(int index)
{
// 獲取指定點的坐標值
return m_ptPosition[index];
}
void CPosition::Serialize(CArchive &ar)
{
CObject::Serialize(ar);
if (ar.IsStoring())
{
// 存儲持久性對象到檔案
ar << m_nCount;
for (int i = 0; i < m_nCount; i++)
ar << m_ptPosition[i];
}
else
{
// 從檔案裝載持久性對象
ar >> m_nCount;
for (int i = 0; i < m_nCount; i++)
ar >> m_ptPosition[i];
}
}
在創建了自定義持久類CPosition后,可以通過該類對鼠標點擊過的點的坐標進行管理,由于序列化的工作已由類本身完成,因此只需在文檔類的Serialize()函數中對CPosition的Serialize()成員函數進行調用即可:
void CSample04Doc::Serialize(CArchive& ar)
{
// 使用定制持久類
m_Position.Serialize(ar);
if (ar.IsStoring())
{
}
else
{
}
}
文件I/O
雖然使用CArchive類內建的序列化功能是保存和加載持久性數據的便捷方式,但有時在程序中需要對文件處理過程擁有更多的控制權,對于這種文件輸入輸出(I/O)服務的
需求,
Windows提供了一系列相關的API函數,并由MFC將其封裝為CFile類,提供了對文件進行打開,關閉,讀,寫,刪除,重命名以及獲取文件信息等文件操作的基本功能,足以處理任意類型的文件操作。CFile類是MFC文件類的基類,支持無緩沖的二進制輸入輸出,也可以通過與CArchive類的配合使用而支持對MFC對象的帶緩沖的序列化。
CFile類包含有一個公有型數據成員m_hFile,該數據成員包含了同CFile類對象相關聯的文件句柄。如果沒有指定句柄,則該值為CFile::hFileNull。由于該數據成員所包含的意義取決于派生的類,因此一般并不建議使用m_hFile。
通過CFile類來打開文件可以采取兩種方式:一種方式是先構造一個CFile類對象然后再調用成員函數Open()打開文件,另一種方式則直接使用CFile類的構造函數去打開一個文件。下面的語句分別演示了用這兩種方法打開磁盤文件“C:\TestFile.txt”的過程:
// 先構造一個實例,然后再打開文件
CFile file;
file.Open(“C:\\TestFile.txt”, CFile::modeReadWrite);
……
// 直接通過構造函數打開文件
CFile file(“C:\\TestFile.txt”, CFile::modeReadWrite);
其中參數CFile::modeReadWrite是打開文件的模式標志,CFile類中與之類似的標志還有十幾個,現集中列表如下:
文件模式標志 說明
CFile::modeCreate 創建方式打開文件,如文件已存在則將其長度設置為0
CFile::modeNoInherit 不允許繼承
CFile::modeNoTruncate 創建文件時如文件已存在不對其進行截斷
CFile::modeRead 只讀方式打開文件
CFile::modeReadWrite 讀寫方式打開文件
CFile::modeWrite 寫入方式打開文件
CFile::shareCompat 在使用過程中允許其他進程同時打開文件
CFile::shareDenyNone 在使用過程中允許其他進程對文件進行讀寫
CFile::shareDenyRead 在使用過程中不允許其他進程對文件進行讀取
CFile::shareDenyWrite 在使用過程中不允許其他進程對文件進行寫入
CFile::shareExclusive 取消對其他進程的所有訪問
CFile::typeBinary 設置文件為二進制模式
CFile::typeText 設置文件為文本模式
這些標志可以通過“或”運算符而同時使用多個,并以此來滿足多種需求。例如,需要以讀寫方式打開文件,如果文件不存在就創建一個新的,如果文件已經存在則不將其文件長度截斷為0。為滿足此條件,可用CFile::modeCreate、CFile::modeReadWrite和CFile::modeNoTruncate等幾種文件模式標志來打開文件:
CFile file ("C:\\TestFile.txt", CFile::modeCreate | CFile::modeReadWrite | CFile::modeNoTruncate);
在打開的文件不再使用時需要將其關閉,即可以用成員函數Close()關閉也可以通過CFile類的析構函數來完成。當采取后一種方式時,如果文件還沒有被關閉,析構函數將負責隱式調用Close()函數去關閉文件,這也表明創建在堆上的CFile類對象在超出范圍后將自動被關閉。由于調用了對象的析構函數,因此在文件被關閉的同時CFile對象也被銷毀,而采取Close()方式關閉文件后,CFile對象仍然存在。所以,在顯式調用Close()函數關閉一個文件后可以繼續用同一個CFile對象去打開其他的文件。
文件讀寫是最常用的文件操作方式,主要由CFile類成員函數Read()、Write()來實現。其函數原型分別為:
UINT Read( void* lpBuf, UINT nCount );
void Write( const void* lpBuf, UINT nCount );
參數lpBuf為指向存放數據的緩存的指針,nCount為要讀入或寫入的字節數,Read()返回的為實際讀取的字節數,該數值小于或等于nCount,如果小于nCount則說明已經讀到文件末尾,可以結束文件讀取,如繼續讀取,將返回0。因此通??梢詫嶋H讀取字節數是否小于指定讀取的字節數或是否為0作為判斷文件讀取是否到達結尾的依據。下面這段代碼演示了對文件進行一次性寫入和循環多次讀取的處理過程:
// 創建、寫入方式打開文件
CFile file;
file.Open("C:\\TestFile.txt", CFile::modeWrite | CFile::modeCreate);
// 寫入文件
memset(WriteBuf, 'a', sizeof(WriteBuf));
file.Write(WriteBuf, sizeof(WriteBuf));
// 關閉文件
file.Close();
// 只讀方式打開文件
file.Open("C:\\TestFile.txt", CFile::modeRead);
while (true)
{
// 讀取文件數據
int ret = file.Read(ReadBuf, 100);
……
// 如果到達文件結尾則中止循環
if (ret < 100)
break;
}
// 關閉文件
file.Close();
Write()和Read()函數執行完后將自動移動文件指針,因此不必再顯示調用Seek()函數去定位文件指針。包含有文件定位函數的完整代碼如下所示:
// 創建、寫入方式打開文件
CFile file;
file.Open("C:\\TestFile.txt", CFile::modeWrite | CFile::modeCreate);
// 寫入文件
memset(WriteBuf, 'a', sizeof(WriteBuf));
file.SeekToBegin();
file.Write(WriteBuf, sizeof(WriteBuf));
// 關閉文件
file.Close();
// 只讀方式打開文件
file.Open("C:\\TestFile.txt", CFile::modeRead);
while (true)
{
// 文件指針
static int position = 0;
// 移動文件指針
file.Seek(position, CFile::begin);
// 讀取文件數據
int ret = file.Read(ReadBuf, 100);
position += ret;
……
// 如果到達文件結尾則中止循環
if (ret < 100)
break;
}
// 關閉文件
file.Close();
小結
持久性和文件I/O是程序保持和記錄數據的一種重要方法,這兩種不同的處理方法雖然功能上有些接近但實現過程卻大不相同。而且這兩種處理方法各有優勢,讀者在編程過程中應根據實際情況而靈活選用。
原文轉自:http://www.kjueaiud.com