Visual C++中對象的序列化與檔案I/O研究

來源:互聯網
上載者:User

Visual C++中對象的序列化與檔案I/O研究

持久性和序列化

  持久性是對象所有的儲存和載入其狀態資料的能力。具有這種能力的對象能夠在應用程式結束之前以某種方式將當前的對象狀態資料記錄下來,當程式再次運行時,通過對這些資料的讀取而恢複到上一次任務結束時的狀態。由於絕大多數的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,COleVariant,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。因此通常可以將實際讀取位元組數是否小於指定讀取的位元組數或是否為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是程式保持和記錄資料的一種重要方法,這兩種不同的處理方法雖然功能上有些接近但實現過程卻大不相同。而且這兩種處理方法各有優勢,讀者在編程過程中應根據實際情況而靈活選用。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.