MFC六大關鍵技術剖析(第四部分)

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其中奧祕幾乎一句不提,目的是讓大家能找到中心,有更簡潔的永久保存的概念,我覺得這種感覺很好!

本文來自CSDN博客,轉載請標明出處:http://blog.csdn.net/liuliye23/archive/2009/12/15/5008664.aspx

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章