永久儲存(串行化)是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類對象。這個問題很容易解決,如下即可:
C++代碼
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,那麼函數就可改寫成如下形式:
if (ar.IsStoring()) //讀寫判斷
ar<<m_MyVar; //寫
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”
//在此調用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
最後再補充講解一下有些抽象的CArchive。我們先看以下程式(注:以下程式包含動态建立等,請包含DECLARE_SERIAL/IMPLEMENT_SERIAL宏)
void MyClass::Serialize(CArchive& ar)
ar<< m_pMyVar; //問題:ar 如何把m_pMyVar所指的對象變量儲存到磁盤?
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其中奧秘幾乎一句不提,目的是讓大家能找到中心,有更簡潔的永久儲存的概念,我覺得這種感覺很好!