永久保存(串行化)是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++代碼
  1. MyDocument:: Serialize(){   
  2.      // 在此函數調用MyClass類的Serialize()就行了!即   
  3.      MyObject. Serialize();           
  4. }  

       3、我們希望MyClass對象為可以動態創建的對象,所以要求在MyClass類中加上DECLARE_DYNAMIC/IMPLEMENT_DYNAMIC宏。

       但目前的Serialize機制還很抽象。我們僅僅知道了表面上的東西,實際又是如何的呢?下面作一個簡單深刻的詳解。

       先看一下我們文檔類的Serialize():

C++代碼
  1. void CMyDoc::Serialize(CArchive& ar)   
  2. {   
  3.     if (ar.IsStoring())   
  4.     {   
  5.         // TODO: add storing code here   
  6.     }   
  7.     else  
  8.     {   
  9.         // TODO: add loading code here   
  10.     }   
  11. }  

       目前這個子數什么也沒做(沒有數據的讀出和寫入),CMyDoc類正等待著我們去改寫這個函數。現在假設CMyDoc有一個MFC可識別的成員變量m_MyVar,那么函數就可改寫成如下形式:

C++代碼
  1. void CMyDoc::Serialize(CArchive& ar)   
  2. {   
  3.     if (ar.IsStoring())     //讀寫判斷   
  4.     {   
  5.         ar<<m_MyVar;        //寫   
  6.     }   
  7.     else  
  8.     {   
  9.         ar>>m_MyVar;        //讀   
  10.     }   
  11. }  

       許多網友問:自己寫的類(即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()函數。

       現在,程序就可以這樣寫:

C++代碼
  1. ……   
  2.   
  3. #include “MyClass.h”   
  4.   
  5. ……   
  6.   
  7. void CMyDoc::Serialize(CArchive& ar)   
  8. {   
  9.     //在此調用MyClass重寫過的Serialize()   
  10.     m_MyObject.Serialize(ar);      // m_MyObject為MyClass實例   
  11. }  

       至此,串行化工作就算完成了,簡單直觀的講:從CObject派生自己的類,重寫Serialize()。在此過程中,我刻意安排:在沒有用到DECLARE_SERIAL/IMPLEMENT_SERIAL宏,也沒有用到CArray等模板類的前提下就完成了串行化的工作。我看過某些書,總是一開始就講DECLARE_SERIAL/IMPLEMENT_SERIAL宏或馬上用CArray模板,讓讀者覺得串行化就是這兩個東西,導致許多朋友因此找不著北。

       大家看到了,沒有DECLARE_SERIAL/IMPLEMENT_SERIAL宏和CArray等數據結構模板也依然可以完成串行化工作。

       CArchive

       最后再補充講解一下有些抽象的CArchive。我們先看以下程序(注:以下程序包含動態創建等,請包含DECLARE_SERIAL/IMPLEMENT_SERIAL宏)

C++代碼
  1. void MyClass::Serialize(CArchive& ar)   
  2. {   
  3.     if (ar.IsStoring())     //讀寫判斷   
  4.     {   
  5.         ar<< m_pMyVar;      //問題:ar 如何把m_pMyVar所指的對象變量保存到磁盤?   
  6.     }   
  7.     else  
  8.     {   
  9.         pMyClass = new MyClass; //準備存儲空間   
  10.         ar>> m_pMyVar;         
  11.     }   
  12. }  

       為回答上面的問題,即“ar<<XXX”的問題,這里給出一段模擬CArchive的代碼。

       “ar<<XXX”是執行CArchive對運算符“<<”的重載動作。ar和XXX都是該重載函數中的一參數而已。函數大致如下:

C++代碼
  1. CArchive& operator<<( CArchive& ar, const CObject* pOb)   
  2. {   
  3.     …………   
  4.     //以下為CRuntimeClass鏈表中找到、識別pOb資料。   
  5.     CRuntimeClass* pClassRef = pOb->GetRuntimeClass();   
  6.     //保存pClassRef即類信息(略)   
  7.            
  8.     ((CObject*)pOb)->Serialize();//保存MyClass數據   
  9.     …………   
  10. }  

       從上面可以看出,因為Serialize()為虛函數,即“ar<<XXX”的結果是執行了XXX所指向對象本身的Serialize()。對于“ar>>XXX”,雖然不是“ar<<XXX”逆過程,大家可能根據動態創建和虛函數的原理料想到它。

       至此,永久保存算是寫完了。在此過程中,我一直努力用最少的代碼,詳盡的解釋來說明問題。以前我為本課題寫過一個版本,并在幾個論壇上發表過,但不知怎么在網上遺失(可能被刪除)。所以這篇文章是我重寫的版本。記得第一個版本中,我是對DECLARE_SERIAL/IMPLEMENT_SERIAL和可串行化的數組及鏈表對象說了許多。這個版本中我對DECLARE_SERIAL/IMPLEMENT_SERIAL其中奧秘幾乎一句不提,目的是讓大家能找到中心,有更簡潔的永久保存的概念,我覺得這種感覺很好!

來自:http://blog.csdn.net/liyi268/article/details/623367

除非特別注明,雞啄米文章均為原創
轉載請標明本文地址:http://www.vkzldl.live/software/273.html
2012年12月4日
作者:雞啄米 分類:軟件開發 瀏覽: 評論:9