1. 程式人生 > >c++物件序列化初步探討

c++物件序列化初步探討

什麼是序列化

程式設計師在編寫應用程式的時候往往需要將程式的某些資料儲存在記憶體中,然後將其寫入某個檔案或是將它傳輸到網路中的另一臺計算機上以實現通訊。這個將程式資料轉化成能被儲存並傳輸的格式的過程被稱為“序列化”(Serialization),而它的逆過程則可被稱為“反序列化”(Deserialization)。

簡單來說,序列化就是將物件例項的狀態轉換為可保持或傳輸的格式的過程。與序列化相對的是反序列化,它根據流重構物件。這兩個過程結合起來,可以輕鬆地儲存和傳輸資料。例如,可以序列化一個物件,然後使用 HTTP 通過 Internet 在客戶端和伺服器之間傳輸該物件。

2      為什麼使用序列化2.1       

哪些情況需要使用序列化2.1.1 以某種儲存形式使自定義物件持久化

通過序列化,可以將物件的狀態保持在儲存媒體中,在以後能夠重新建立精確的副本。我們經常需要將物件的欄位值儲存到磁碟中,並在以後檢索此資料。儘管不使用序列化也能完成這項工作,但這種方法通常很繁瑣而且容易出錯,並且在需要跟蹤物件的層次結構時,會變得越來越複雜。可以想象一下編寫包含大量物件的大型業務應用程式的情形,程式設計師不得不為每一個物件編寫程式碼,以便將欄位和屬性儲存至磁碟以及從磁碟還原這些欄位和屬性。序列化提供了輕鬆實現這個目標的快捷方法。

2.1.2 將物件從一個地方傳遞到另一個地方

通常來說,物件僅在建立物件的應用程式域中有效。但是,序列化可以通過值將物件從一個應用程式域傳送到另一個應用程式域中。例如,序列化可用於在ASP.NET中儲存會話狀態並將物件複製到Windows窗體的剪貼簿中。序列化最重要的目的之一就是在網路上傳輸物件。

2.2          序列化的優勢

在系統化的序列化方法出現之前,程式設計師如果想要將自定義的一個類的物件持久化地儲存下來,並進行傳輸,可以採用以下這些方法:

l  由程式設計師自己實現儲存物件資料的功能,針對每一個物件編寫程式碼,將其資料儲存下來。

l  將物件強制轉換為char*或者void*型別的資料,然後進行資料的傳輸。

下面將從通用性、便捷性、靈活性和可移植性的角度來比較序列化相對於上述兩種方法的優勢。

2.2.1 通用性

如果由程式設計師自己實現儲存物件資料的功能,那麼對於每一個類的物件,程式設計師都要編寫不同的程式碼,工作量很大,通用性不高。而序列化提供了一套流程化的方法,對於每一種類,都是大體一致的流程,提高了程式碼的通用性。

如果將物件強制轉換為char*或void*型別的資料進行傳輸,那麼必須預先得知該物件的大小以提前分配陣列的空間。但是,如果該物件中存在可變長的資料結構,就無法準確地得知物件資料的大小了,只能預先估計一下。如果估計小了,可能會造成空間溢位,程式崩潰的後果;如果估計大了,又會造成空間的浪費。但是,如果使用序列化的方法,就能很好地解決可變長資料結構的問題。

2.2.2 便捷性

    如果由程式設計師自己實現儲存物件資料的功能,那麼對於類中不同的資料結構,程式設計師都要編寫相應的儲存程式碼,簡單的資料結構還好說,如果是具有多種層次的資料結構,程式碼的編寫將越來越複雜,這樣繁瑣且容易出錯。序列化提供了針對簡單資料型別,以及字串型別、STL容器、指標等種種資料型別的持久化的方法,只需簡單地呼叫即可,具有很大的便捷性。

2.2.3 靈活性

    序列化提供了若干種將物件資料持久化的格式,比如以簡單文字格式儲存、以XML格式儲存、以SOAP格式儲存、以二進位制格式儲存等等。還提供了多種儲存持久化之後的物件的方式,比如儲存到字串、儲存到檔案等等,具有很大的靈活性。

2.2.4 可移植性

使用將物件強制轉換為char*型別進行傳輸的方法,需要注意CPU位元組序的問題。如果起始機器與目的機器的CPU位元組序不同,就會造成目的機器讀到的資料無法恢復成原來物件的問題。雖然可以通過將本地位元組序轉化為網路位元組序進行傳輸,傳到目的機器之後再將網路位元組序轉為本地位元組序的方法解決這個問題,但是這就增加了程式設計師考慮問題的複雜性。序列化遮蔽了位元組序的差異,使得被持久化物件的傳輸更具有可移植性。

此外,使用序列化還可以很好地跨平臺。

4 使用C++將物件進行序列化的幾種方法

使用C++進行物件序列化的方法可以有以下三種:基於Boost庫的方法;基於.Net Framework的方法;以及基於MFC的方法。本章將就三種方法的實現機制、實現步驟,以及注意事項進行說明。

由於我們的開發環境在Windows下,部署環境在Unix下,因此我們的開發需要使用兩個平臺都可以相容的技術。經過驗證,基於.Net和基於MFC的方法僅適用於Windows的環境,而Boost庫在Windows和Unix下都有相應的版本,因此在專案中應優先考慮使用Boost庫進行物件的序列化。儘管如此,本文中仍然列出使用.Net和MFC進行序列化的方法,以供參考。三種方法相應的程式碼實現的例子將附在文章之後。

4.1 使用Boost庫4.1.1 實現機制

這裡,我們用術語序列化(serialization)來表示將一組原始的C++資料結構表示為位元組流達到可逆析構的目的。這樣的系統可以用來在另一個程式環境中重新建立原來的資料結構。因此,它也可以作為物件永續性(object persistence),遠端引數傳遞(remote parameter passing),或者其他特性的實現基礎。在我們的系統中,將使用術語檔案(archive)表示一個具體的位元組流。檔案可以是二進位制檔案,文字檔案,XML檔案,或者其他使用者定義的型別。  

Boost序列化庫的目標是:

l  程式碼的可移植性–只依靠ANSI C++的特性。

l  程式碼的經濟性–挖掘各種C++的特性如RTTI、模板、和多繼承等等使使用者容易使用並且程式碼短小。

l  類版本的獨立性。–當一個類的定義改變時,老版本的類的檔案仍然可以被匯入新版本的類中。

l   指標的深度儲存和恢復。–儲存或恢復指標的同時儲存或恢復指標指向的資料。

l  正確的處理多個指標指向相同物件時的問題。

l  對STL和其他常用模板類的序列化的直接支援。

l  資料的可移植性–在一個平臺上建立的位元組流在另一個平臺上也應該是正確的。

l  序列化和檔案格式的正交性–可以在不改變類的序列化部分時應用任何格式的檔案作為檔案。

l  支援非侵入(Non-intrusive)式的實現。類不需要從某個特定的類派生或者實現特定的成員函式。這對於我們不能或不願意修改類的定義的情況時是相當必要的。

l  檔案的介面應該足夠簡單使建立新型別的檔案的工作變得輕鬆。

l  檔案應該支援XML格式。

   Boost中,與序列化有關的兩個庫是Archive庫和Serialization庫。

4.1.2 實現步驟

首先,為被序列化的類實現一個對應的serialize(Archive & ar, const unsigned int version)方法;

其次,構造boost::archive::text_oarchive類或其他archive輸出類的物件,並將其關聯到一個輸出流,利用<<運算子將被序列化的物件輸出到某個文件中;

最後,構造boost::archive::text_iarchive類或其他archive輸入類的物件,並將其關聯到一個輸入流,讀入資料,利用>>運算子會付出被序列化的物件。

4.1.3 注意事項

使用這種方法需要注意的是:

l  Boost從1.32版本之後才提供對序列化的支援,所以一定要用版本在1.32之後的;

l  Boost中的Serialization庫需要編譯之後得到庫檔案才能使用,並加入專案的附加依賴項中才可使用;

l  根據需要包含boost/serialization和boost/archive下的一些標頭檔案。

4.2 使用.NET4.2.1 實現機制

.NET的執行時環境用來支援使用者定義型別的流化的機制。它在此過程中,先將物件的公共欄位和私有欄位以及類的名稱(包括類所在的程式集)轉換為位元組流,然後再把位元組流寫入資料流。在隨後對物件進行反序列化時,將創建出與原物件完全相同的副本。

.Net框架對序列化機制具有非常好的支援,它提供了兩個名字空間(namespace):System.Runtime.Serialization和System.Runtime.Serialization.Formatters以完成序列化機制的大部分功能。

序列化機制的實現是依靠格式器(Formatter)而完成的,它是一個從System.Runtime.Serialization.IFormatter繼承下來的類的物件。格式器完成了將程式資料轉化到能被儲存並傳輸的格式的工作,同時也完成了將資料轉化回來的工作。.Net框架為程式設計師提供了兩種型別的格式器,一種通常是應用於桌面型別的應用程式的,它一個是System.Runtime.Serialization.Formatters.Binary.BinaryFormatter類的物件,而另一種則更主要的應用於.Net Remoting和XML Web服務等領域的,它一個是System.Runtime.Serialization.Formatters.Soap.SoapFormatter類的物件。從它們的名稱來看,不妨將它們分別稱為二進位制格式器和XML格式器。它們對應於.Net提供的兩種序列化技術:

二進位制序列化保持型別保真度,這對於在應用程式的不同調用之間保留物件的狀態很有用。例如,通過將物件序列化到剪貼簿,可在不同的應用程式之間共享物件,可以將物件序列化到流、磁碟、記憶體和網路等等。它的優點在於可以將所有的物件成員都儲存下來,並且效能優於XML序列化。

XML 序列化僅序列化公共屬性和欄位,且不保持型別保真度。當您要提供或使用資料而不限制使用該資料的應用程式時,這一點是很有用的。由於 XML 是一個開放式標準,因此,對於通過 Web 共享資料而言,這是一個很好的選擇。SOAP 同樣是一個開放式標準,這使它也成為一個頗具吸引力的選擇。它的優點在於互操作性好,可讀性強。

4.2.2 實現步驟

使用.Net下的二進位制序列化方法進行物件序列化的步驟如下:

首先,要使用 Serializable 屬性對物件的類進行標記;

其次,利用BinaryFormatter的Serialize方法將物件寫入到一個檔案流中;

最後,利用BinaryFormatter的DeSerialize方法讀取檔案流,恢復物件。

4.2.3 注意事項

使用這種方法需要注意的是:

l  需要使用System::Runtime::Serialization::Formatters::Binary名稱空間和 System::Runtime::Serialization名稱空間;

l  被序列化的類在宣告時必須標識[Serializable]屬性;

l  所涉及的類必須是託管類,即類的宣告前需要有ref關鍵字,用gcnew關鍵字表示在託管堆上分配記憶體,指標符號用^來標識等。

4.3 使用MFC4.3.1 實現機制

物件的序列化歸根結底是將物件的資料寫入載體,再重新讀取為物件的過程。MFC中對資料的讀寫創造了十分好的支援,這使得我們可以十分方便的利用MFC的資料讀寫類來實現物件序列化的需要。

MFC 為資料讀寫設計了三個基本的類——CFile(CFile類)、CStdioFile(標準I/O檔案類)、CArchive(CArchive類)。其中標準CStdioFile類提供相當於C的流式檔案的功能,可以用文字或者二進位制方式開啟,可以被緩衝。CFile類提供了非緩衝的二進位制輸入輸出檔案,它既可以與CArchive類結合實現VisualC++設計中常用的檔案序列化,也可以由設計者自己訂製儲存方案,實現資料的讀寫操作(此方法的相容問題需要解決,保密性強)。CArchive類是VisualC++程式設計中最常用的檔案處理的方法,CArchive類不僅可以實現簡單資料結構的讀寫操作,還可以通過對CObiect類的派生實現對複雜資料結構的讀寫操作,因此,利用CArchive類,可以輕鬆實現任意資料結構的序列化。

4.3.2 實現步驟

實現序列化的的類需要滿足一系列條件:

1. 該類需要從CObject類派生(可以是間接派生);

2. 在類中中進行DECLARE_SERIAL巨集定義;

3. 類存在有預設的建構函式;

4. 類中實現了Serialize(CArchive&)函式,並且在其中呼叫基類的序列化函式;

5. 使用IMPLEMENT_SERIAL巨集指明類名及版本號。

滿足了這些條件之後,就可以進行序列化與反序列化了。

序列化時,首先,例項化一個CArchive類的物件,將其與輸出檔案相關聯;其次,利用CArchive類的<<運算子過載將需要序列化的物件儲存在檔案中。

反序列化時,將CArchive類的物件與儲存物件的檔案相關聯;然後新建一個需要反序列化的物件,利用CArchive類的>>運算子過載將檔案裡的內容恢復到需要反序列化的物件中。

4.3.3 注意事項

使用這種方法需要注意的是:

l  需要包含afx.h標頭檔案;

l  它不支援string型別的序列化,但是支援CString型別的序列化;

l  需要將專案屬性中的MFC屬性配置為“在共享DLL中使用MFC”或“在靜態庫中使用MFC”,否則編譯時會報錯。

5 使用Boost庫進行物件序列化的關鍵技術5.1 基礎

1、基本型別的存檔和讀取

對基本型別. 直接使用以下語句就可以完成存檔或讀取:

l  用 ar << data或ar & data;  寫入存檔

l  用 ar >> data或ar & data;  從存檔取出

2、自定義型別的存檔和讀取

對自定義型別. 則會呼叫 serialize() 函式,serialize 函式用來“儲存/裝載”其資料成員。這個處理採用遞迴的方式,直到所有包含在類中的資料“被儲存/被裝載”。

l  侵入式:  t.serialize(ar, version)

l  非侵入式:  serialize(ar, t, version)

3、所需包含的標頭檔案:

l  以簡單文字格式實現存檔:text_oarchive和text_iarchive

l  寬字元的文字格式存檔 :text_woarchive  text_wiarchive

l  xml存檔:xml_oarchive  xml_iarchive

l   使用寬字元的xml文件(用於utf-8)輸出:xml_woarchive    xml_wiarchive

l  二進位制的存檔 (注意 二進位制存檔是不可移植的):binary_oarchive   binary_iarchive

5.2 侵入式和非侵入式

對於被序列化的類,有兩種實現其對應的serialize方法的方式,一種是侵入式,即把serialize方法作為被序列化類的一個成員方法來實現;另一種是非侵入式,即將serialize方法放在另一個名字空間下,作為被序列化類的一個友元方法來實現。在不可修改被序列化的類的程式碼的情況下,應該採用非侵入式的方式。

侵入式的例子:

class MyPoint

{

    int mX;
    int mY;

private:
    friend class  boost::serialization::access;   //侵入式版本的要加這個.

    //存入和讀取都使用下邊的 serialize() 函式.
    //其中的 Archive 是一個輸入或輸出的文件.  當輸入的時候 & 為 >> . 當輸出的時候 & 為 <<.
    template<class Archive>
    void serialize(Archive& ar, const unsigned int version)
    {
        ar & mX;       //序列化資料成員
        ar & mY;
    }

public:
    MyPoint() {}
    MyPoint(int x, int y) : mX(x), mY(y) {}  
};

非侵入式的例子:

class MyPoint
{
private:        

// 注意關鍵字”friend”和多了一個類引用作引數

     template<class Archive>

friend void serialize(Archive& ar, MyPoint&, unsigned int const);

    int mX;
    int mY;
public:
    MyPoint() {}
    MyPoint(int x, int y) : mX(x), mY(y) {}
};
//非侵入式
namespace boost {                  //實現放在這個名字空間下
namespace serialization {

template<class Archive>
void serialize(Archive & ar, MyPoint& p, const usigned int version)
{
    ar & p.mX & p.mY;   //可以連著 &
}

}
}   //namespace  結束

5.3 派生類的序列化

對派生類進行序列化需要有一個前提,即它的父類必須也實現了serialize方法,也可以序列化。如果在派生類的父類沒有實現serialize方法,僅對派生類進行序列化,將不能儲存派生類從父類繼承下來的資料資訊,而僅能儲存屬於派生類自身的資料資訊。

對派生類進行序列化的步驟是:

1、包含boost/serialization/base_object.hpp標頭檔案;

2、在serialize模版方法中,使用ar & boost::serialization::base_object<父類>(*this)這樣的語法來儲存父類的資料,不能直接呼叫父類的serialize函式。

一個例子如下:

#include <boost/serialization/base_object.hpp> //一定要包含此標頭檔案

class B:A

{

    friend class boost::serialization::access;

    char c;

    template<class Archive>

    void serialize(Archive & ar, const unsigned int version)

    {

        ar & boost::serialization::base_object<A>(*this);//注意這裡

        ar & c;

    }

public:

    …

};  

5.4 陣列的序列化

對於陣列進行序列化,就是儲存陣列中的每一個數據成員,因此相當於對陣列中的每一個數據成員做序列化。可以用以下形式:

for(int i = 0; i < sizeof(array); i++)

{

  ar & array[i];

}

但是事實上,Boost的序列化庫能檢測出被序列化的物件是一個數組,將產生上述等價的程式碼,例子如下:

class bus_route

{

    friend class boost::serialization::access;

    bus_stop stops[10];

    template<class Archive>

    void serialize(Archive & ar, const unsigned int version)

    {

        ar & stops;

    }

public:

    bus_route(){}

};

5.5 指標的序列化

序列化整個物件是要求在另一個地方和時間重新構造原始資料結構。在使用指標的情況下,為了達到重新構造原始資料結構的目的,僅僅儲存指標的值是不夠的,指標指向的物件也必須被儲存。當成員最後被裝載,一個新的物件被建立,指向新的物件的新的指標被裝載到類的成員中。

所有這一切由Boost的序列化庫自動完成,程式設計師只需直接序列化指標即可。(說是這麼說,使用要慎重,因為例子並沒有調通。)一個例子如下:

class bus_route{    friend class boost::serialization::access;    bus_stop * stops[10];    template<class Archive>    void serialize(Archive & ar, const unsigned int version)    {        int i;        for(i = 0; i < 10; ++i)            ar & stops[i];    }public:    bus_route(){}};5.6 對STL容器的序列化

對於STL容器,比如vector或list,需要在標頭檔案中包含<boost/serialization/vector.hpp>或<boost/serialization/list.hpp>等,然後就可以直接進行序列化了。一個例子如下:

#include <boost/serialization/list.hpp>class bus_route{    friend class boost::serialization::access;    std::list<bus_stop *> stops;    template<class Archive>    void serialize(Archive & ar, const unsigned int version)    {        ar & stops;    }public:    bus_route(){}};5.7 被序列化的類的成員是其他類的物件的情況

如果被序列化的類有成員是其他類的物件,那麼,只有在其物件成員的類也實現了serialize方法並能被序列化的情況下,該類才能被序列化。

比如前幾個例子中,類bus_route中有成員是bus_stop類的物件。那麼只有bus_stop類實現了serialize方法後,bus_route類才能被序列化。

5.8 輸出

Boost的序列化庫可以以三種格式進行輸出,分別是:簡單文字格式、XML格式,以及二進位制格式。其中每種格式又可以輸出到c++的ostream流中,比如,ostringstream(字串輸出流),ofstream(檔案輸出流)。下例是一個以簡單文字格式輸出到字串流中的例子。

//序列化,輸出到字串

         std::ostringstream ossOut(ostringstream::out);   //把物件寫到字串輸出流中

         boost::archive::text_oarchive oa(ossOut);

         TestClass objTestClass;

oa << objTestClass;

string strTrans = ossOut.str();

……

//反序列化,從字串輸入

istringstream ossIn(strTrans);      //從字串輸入流中讀入資料

         boost::archive::text_iarchive ia(ossIn);

         TestClass newObjTestClass;

         ia >> newObjTestClass;

6 結論

1、  在基於OTT結構的資料庫結構的效能測試中,針對資料庫中的每一個表,定義了一個相應的類,我們的目標是將這些類的物件進行序列化。但是,在試圖序列化的過程中遇到一個問題,即:所有的OTT表的類都繼承自一個由Oracle庫檔案定義的類oracle::occi::PObject。而派生類的序列化要求其父類也必須實現序列化介面,否則就會派生類繼承的父類的成員就會在序列化時丟失(見5.3節)。這就要求修改庫檔案,是PObject也實現序列化介面。可是貿然地修改庫檔案可能會導致連鎖反應,引起其他引用庫檔案的程式出錯,此外,還有智慧財產權的問題。所以,使用Boost序列化庫來對OTT表的類進行序列化的路可能走不通。應考慮其他方法。

2、  在使用共享記憶體傳遞物件資料時,可以將物件資料以簡單文字格式進行序列化,再用ostringstream流輸出到字串中,進行傳遞,完全可行。

7 附錄7.1 資源

1、Boost中Serialization庫的文件:http://www.boost.org/doc/libs/1_37_0/libs/serialization/doc/index.html

2、Boost序列化庫教程:http://dozb.bokee.com/1692310.html#derivedclasses

3、Learning boost 1 Serializationhttp://blog.csdn.net/freerock/archive/2007/08/17/1747928.aspx

4、 C++中使用boost::serialization庫――應用篇:http://www.cnblogs.com/mslk/archive/2005/11/25/284491.html

5、 C++ Reference: IOstream Library: ostream:http://www.cplusplus.com/reference/iostream/ostream/

7.2 程式示例對照表

l  CplusSerializeBoost:使用Boost的序列化庫進行序列化;

l  CplusSerializeDotNet:使用.Net進行序列化;

l  CplusSerializeMFC:使用MFC進行序列化。