MFC?六大關鍵技術?(?第四部分?)?——永久保存(串行化)
先用一句話來說明永久保存的重要:弄懂它以后,你就越來越像個程序員了!
如果我們的程序不需要永久保存,那幾乎可以肯定是一個小玩兒。那怕我們的記事本、畫圖等小程序,也需要保存才有真正的意義。
對于?MFC?的很多地方我不甚滿意,總覺得它喜歡拿一組低能而神秘的宏來故弄玄虛,但對于它的連續存儲(?serialize?)機制,卻是我十分鐘愛的地方。在此,可讓大家感受到面向對象的幸福。
MFC?的連續存儲(?serialize?)機制俗稱串行化。“在你的程序中盡管有著各種各樣的數據,?serialize?機制會象流水一樣按順序存儲到單一的文件中,而又能按順序地取出,變成各種不同的對象數據。”不知我在說上面這一句話的時候,大家有什么反應,可能很多朋友直覺是一件很簡單的事情,只是說了一個“爽”字就沒有下文了。
要 實現象流水一樣存儲其實是一個很大的難題。試想,在我們的程序里有各式各樣的對象數據。如畫圖程序中,里面設計了點類,矩形類,圓形類等等,它們的繪圖方 式及對數據的處理各不相同,用它們實現了成百上千的對象之后,如何存儲起來?不想由可,一想頭都大了:我們要在程序中設計函數?store()?,在我們單擊“文件?/?保存”時能把各對象往里存儲。那么這個?store()?函數要神通廣大,它能清楚地知道我們設計的是什么樣的類,產生什么樣的對象。大家可能并不覺得這是一件很困難的事情,程序有能力知道我們的類的樣子,對象也不過是一塊初始化了存儲區域罷了。就把一大堆對象“轉換”成磁盤文件就行了。
即使上面的存儲能成立,但當我們單擊“文件?/?打開”時,程序當然不能預測用戶想打開哪個文件,并且當打開文件的時候,要根據你那一大堆垃圾數據?new?出數百個對象,還原為你原來存儲時的樣子,你又該怎么做呢?
試 想,要是我們有一個能容納各種不同對象的容器,這樣,用戶用我們的應用程序打開一個磁盤文件時,就可以把文件的內容讀進我們程序的容器中。把磁盤文件讀進 內存,然后識別它“是什么對象”是一件很難的事情。首先,保存過程不像電影的膠片,把景物直接映射進去,然后,看一下膠片就知道那是什么內容。可能有朋友 說它象錄像磁帶,拿著錄像帶我們看不出里面變化的磁場信號,但經過錄像機就能把它還原出來。
其實不是這樣的,比如保存一個矩形,程序并不是把矩形本身按點陣存儲到磁盤中,因為我們繪制矩形的整個過程只不過是調用一個?GDI?函數罷了。它保存只是坐標值、線寬和某些標記等。程序面對“?00 FF?”這樣的東西,當然不知道它是一個圓或是一個字符!
拿 剛才錄像帶的例子,我們之所以能最后放映出來,前提我們知道這對象是“錄像帶”,即確定了它是什么類對象。如果我們事先只知道它“里面保存有東西,但不知 道它是什么類型的東西”,這就導致我們無法把它讀出來。拿錄像帶到錄音機去放,對錄音機來說,那完全是垃圾數據。即是說,要了解永久保存,要對動態創建有 深刻的認識。
現在大家可以知道困難的根源了吧。我們在寫程序的時候,會不斷創造新的類,構造新的對象。這些對象,當然是舊的類對象(如?MyDocument?)從未見過的。那么,我們如何才能使文檔對象可以保存自己新對象呢,又能動態創建自己新的類對象呢?
許多朋友在這個時候想起了?CObject?這個類,也想到了虛函數的概念。于是以為自己“大致了解”串行化的概念。他們設想:“我們設計的?MyClass?(我們想用于串行化的對象)全部從?CObject?類派生,?CObject?類對象當然是?MyDocument?能認識的。”這樣就實現了一個目的:本來?MyDocument?不能識別我們創建的?MyClass?對象,但它能識別CObject?類對象。由于?MyClass?從?CObject?類派生,我產的新類對象“是一個?CObject?”,所以?MyDocument?能把我們的新對象當作?CObiect?對象讀出。或者根據書本上所說的:打開或保存文件的時候,?MyDocument?會調用?Serialize?(),?MyDocument?的?Serialize?()函會呼叫我們創建類的?Serialize?函數?[?即是在MyDocument Serialize?()中調用:m_pObject?->?Serialize()?,注意:在此m_pObject?是CObject?類指針,它可以指向我們設計的類對象]?。最終結果是?MyDocument?的讀出和保存變成了我們創建的類對象的讀出和保存,這種認識是不明朗的。
有意思還有,在網上我遇到幾位自以為懂了?Serialize?的朋友,居然不約而同的犯了一個很低級得讓人不可思議的錯誤。他們說:?Serialize?太簡單了!?Serialize?()是一個虛函數,虛函數的作用就是“優先派生類的操作”。所以?MyDocument?不實現?Serialize?()函數,留給我們自己的?MyClass?對象去調用?Serialize?()……真是哭笑不得,我們創建的類?MyClass?并不是由?MyDocument?類派生,?Serialize?()函數為虛在?MyDocument?和?MyClass?之間沒有任何意義。?MyClass?產生的?MyObject?對象僅僅是?MyDocument?的一個成員變量罷了。
話說回來,由于?MyClass?從?CObject?派生,所以CObject?類型指針能指向?MyClass?對象,并且能夠讓?MyClass?對象執行某些函數(特指重載的?CObject?虛函數),但前提必須在?MyClass?對象實例化了,即在內存中占領了一塊存儲區域之后。不過,我們的問題恰恰就是在應用程序隨便打開一個文件,面對的是它不認識的?MyClass?類,當然實例化不了對象。
幸好我們在上一節課中懂得了動態創建。即想要從CObject?派生的MyClass?成為可以動態創建的對象只要用到DECLARE_DYNAMIC/IMPLEMENT_DYNAMIC?宏就可以了(注意:最終可以Serialize?的對象僅僅用到了DECLARE_SERIAL/IMPLEMENT_SERIAL?宏,這是因為DECLARE_SERIAL/IMPLEMENT_SERIAL?包含了DECLARE_DYNAMIC/IMPLEMENT_DYNAMIC?宏)。
從解決上面的問題中,我們可以分步理解了:
1、???Serialize?的目的:讓?MyDocument?對象在執行打開?/?保存操作時,能讀出(構造)和保存它不認的?MyClass?類對象。
2、???MyDocument?對象在執行打開?/?保存操作時會調用它本身的?Serialize?()函數。但不要指望它會自動保存和讀出我們的?MyClass?類對象。這個問題很容易解決,就直接在?MyDocument::?Serialize?(){
//?在此函數調用MyClass?類的Serialize?()就行了!即
MyObject. Serialize?();???????
}
3、???我們希望?MyClass?對象為可以動態創建的對象,所以要求在MyClass?類中加上DECLARE_DYNAMIC/IMPLEMENT_DYNAMIC?宏。
但目前的Serialize?機制還很抽象。我們僅僅知道了表面上的東西,實際又是如何的呢?下面作一個簡單深刻的詳解。
先看一下我們文檔類的Serialize?()
void CMyDoc::Serialize(CArchive& ar)
{
????if (ar.IsStoring())
????{
????????// TODO: add storing code here
????}
????else
????{
????????// TODO: add loading code here
????}
}
目前這個子數什么也沒做(沒有數據的讀出和寫入),CMyDoc?類正等待著我們去改寫這個函數。現在假設CMyDoc?有一個MFC?可識別的成員變量m_MyVar,?那么函數就可改寫成如下形式:
void CMyDoc::Serialize(CArchive& ar)
{
????if (ar.IsStoring())?????//?讀寫判斷
????{
????????ar<<m_MyVar;????????//?寫
????}
????else
????{
????????ar>>m_MyVar;????????//?讀
????}
}
許多網友問:自己寫的類(即?MFC?未包含的類)為什么不行?我們在?CMyDoc?里包含自寫類的頭文件MyClass.h?,這樣CMyDoc?就認識MyDoc?類對象了。這是一般常識性的錯誤,MyDoc?類認識MyClass?類對象與否并沒有用,關鍵是CArchive?類,即對象ar?不認識MyClass?(當然你夢想重寫CArchive?類當別論)。“>>?”、“<<?”都是CArchive?重載的操作符。上面ar>>m_MyVar?說白即是在執行一個以ar?和m_MyVar?為參數的函數,類似于function(ar,m_MyVar)?罷了。我們當然不能傳遞一個它不認識的參數類型,也因此不會執行function(ar,m_MyObject)?了。
[?注:這里我們可以用指針。讓MyClass?從Cobject?派生,一切又起了質的變化,假設我們定義了:MyClass *pMyClass = new MyClass;?因為MyClass?從CObject?派生,根據虛函數原理,pMyClass?也是一個CObject*?,即pMyClass?指針是CArchive?類可認識的。所以執行上述function(ar, pMyClass)?,即ar << pMyClass?是沒有太多的問題(在保證了MyClass?對象可以動態創建的前提下)。]
?
回過頭來,如果想讓?MyClass?類對象能?Serialize?,就得讓MyClass?從CObject?派生,Serialize?()函數在CObject?里為虛,MyClass?從CObject?派生之后就可以根據自己的要求去改寫它,象上面改寫CMyDoc::Serialize?()方法一樣。這樣MyClass?就得到了屬于MyClass?自己特有的Serialize?()函數。
現在,程序就可以這樣寫:
……
#include “MyClass.h”
……
void CMyDoc::Serialize(CArchive& ar)
{
????//?在此調用?MyClass?重寫過的?Serialize()
????m_MyObject. Serialize(ar);??????// m_MyObject?為?MyClass?實例
}
至此,串行化工作就算完成了,一即簡單直觀:從?CObject?派生自己的類,重寫?Serialize?()?。在此過程中,我刻意安排:在沒有用到?DECLARE_SERIAL/IMPLEMENT_SERIAL?宏,也沒有用到CArray?等模板類的前提下就完成了串行化的工作。我看過某些書,總是一開始就講DECLARE_SERIAL/IMPLEMENT_SERIAL?宏或馬上用CArray?模板,讓讀者覺得串行化就是這兩個東西,導致許多朋友因此找不著北。
大家看到了,沒有DECLARE_SERIAL/IMPLEMENT_SERIAL?宏和CArray?等數據結構模板也依然可以完成串行化工作。
?
現在可以騰出時間講一下大家覺得十分抽象的?CArchive?。我們先看以下程序(注:以下程序包含動態創建等,請包含DECLARE_SERIAL/IMPLEMENT_SERIAL?宏)
void?MyClass?::Serialize(CArchive& ar)
{
????if (ar.IsStoring())?????//?讀寫判斷
????{
????????ar<< m_pMyVar;??????//?問題:ar?如何把m_pMyVar?所指的對象變量保存到磁盤?
????}
????else
????{
????????pMyClass = new MyClass; //?準備存儲空間
????????ar>> m_pMyVar;?????
????}
}
要回答上面的問題,即“?ar<<XXX?”的問題。和?我們得看一下模擬?CArchive?的代碼。
“ar<<XXX?”是執行CArchive?對運算符“<<?”的重載動作。ar?和XXX?都是該重載函數中的一參數而已。函數大致如下:
CArchive& operator<<( CArchive& ar, const CObject* pOb)
{
????…………
????????//?以下為CRuntimeClass?鏈表中找到、識別pOb?資料。
????????CRuntimeClass* pClassRef = pOb->GetRuntimeClass();
????????//?保存pClassRef?即類信息(略)
???????
????????((CObject*)pOb)->Serialize();//?保存MyClass?數據
????…………
}
從上面可以看出,因為?Serialize()?為虛函數,即“ar<<XXX?”的結果是執行了XXX?所指向對象本身的Serialize()?。對于“ar>>XXX?”,雖然不是“ar<<XXX?”逆過程,大家可能根據動態創建和虛函數的原理料想到它。
至此,永久保存算是寫完了。在此過程中,我一直努力用最少的代碼,詳盡的解釋來說明問題。以前我為本課題寫過一個版本,并在幾個論壇上發表過,但不知怎么在網上遺失(可能被刪除)。所以這篇文章是我重寫的版本。記得第一個版本中,我是對DECLARE_SERIAL/IMPLEMENT_SERIAL?和可串行化的數組及鏈表對象說了許多。這個版本中我對DECLARE_SERIAL/IMPLEMENT_SERIAL?其中奧秘幾乎一句不提,目的是讓大家能找到中心,有更簡潔的永久保存的概念,我覺得這種感覺很好!
摘自:http://blog.csdn.net/liyi268/archive/2006/03/13/623367.aspx