TN002:持續性物件資料格式
此附注描述 MFC 常式,這些常式支援持續性 C++ 物件,以及儲存在檔案中的物件資料格式。 這只適用于具有 DECLARE_SERIAL 和 IMPLEMENT_SERIAL 宏的類別。
問題
持續性資料的 MFC 實作會將許多物件的資料儲存在檔案的單一連續部分中。 物件的 Serialize
方法會將物件的資料轉譯成壓縮的二進位格式。
實作會使用 CArchive 類別 ,保證所有資料都以相同的格式 儲存。 它會使用 CArchive
物件作為翻譯工具。 在您呼叫 CArchive::Close 之前,此物件會從建立物件的時間保存。 當程式結束包含 CArchive
的範圍時,程式設計人員可以明確呼叫這個方法,或由解構函式隱含呼叫。
此附注描述 CArchive::ReadObject 和 CArchive::WriteObject 成員 的 CArchive
實作。 您可以在 Arcobj.cpp 中找到這些函式的程式碼,以及 Arccore.cpp 中的主要實 CArchive
作。 使用者程式碼不會直接呼叫 ReadObject
和 WriteObject
。 相反地,這些物件會由類別特定的型別安全插入和擷取運算子使用,這些運算子是由DECLARE_SERIAL和IMPLEMENT_SERIAL宏自動產生的。 下列程式碼示範如何 WriteObject
以隱含方式呼叫 和 ReadObject
:
class CMyObject : public CObject
{
DECLARE_SERIAL(CMyObject)
};
IMPLEMENT_SERIAL(CMyObj, CObject, 1)
// example usage (ar is a CArchive&)
CMyObject* pObj;
CArchive& ar;
ar <<pObj; // calls ar.WriteObject(pObj)
ar>> pObj; // calls ar.ReadObject(RUNTIME_CLASS(CObj))
將物件儲存至存放區 (CArchive::WriteObject)
方法 CArchive::WriteObject
會寫入用來重新建構物件的標頭資料。 此資料包含兩個部分:物件的型別和物件的狀態。 這個方法也會負責維護要寫出之物件的識別,以便只儲存單一複本,而不論該物件的指標數目(包括迴圈指標)。
儲存(插入)和還原(擷取)物件依賴數個「資訊清單常數」。這些值會儲存在二進位檔中,並提供封存的重要資訊(請注意 「w」 前置詞表示 16 位數量):
標記 | 描述 |
---|---|
wNullTag | 用於 Null 物件指標 (0)。 |
wNewClassTag | 指出下列類別描述是這個封存內容 (-1) 的新功能。 |
wOldClassTag | 表示在此內容中已經看到正在讀取之物件的類別(0x8000)。 |
儲存物件時,封存會 維護 CMapPtrToPtr ( m_pStoreMap ),這是從預存物件到 32 位持續性識別碼 (PID) 的對應。 PID 會指派給每個唯一物件,以及儲存在封存內容中的每個唯一類別名稱。 這些 PID 會從 1 開始循序發出。 這些 PID 在封存範圍之外沒有意義,特別是不會與記錄號碼或其他身分識別專案混淆。
在 類別中 CArchive
,PID 是 32 位,但是除非它們大於0x7FFE,否則會寫成 16 位。 大型 PID 會寫入為0x7FFF,後面接著 32 位 PID。 這會與在舊版中建立的專案保持相容性。
提出將物件儲存至封存的要求時(通常是使用全域插入運算子),就會檢查 Null CObject 指標。 如果指標為 Null,則會 將 wNullTag 插入封存資料流程中。
如果指標不是 Null,而且可以序列化(類別是類別 DECLARE_SERIAL
),程式碼會 檢查m_pStoreMap 以查看物件是否已儲存。 如果有,程式碼會將與該物件相關聯的 32 位 PID 插入封存資料流程。
如果物件之前尚未儲存,有兩種可能考慮:物件和物件確切類型(也就是類別)都是這個封存內容的新手,或者物件是已經看到的確切類型。 若要判斷是否已看到類型,程式碼會查詢 CRuntimeClass 物件的m_pStoreMap ,該物件符合 CRuntimeClass
與所儲存物件相關聯的物件。 如果有相符專案, WriteObject
請插入 wOldClassTag 和此索引的 位 OR
標記。 CRuntimeClass
如果 是這個封存內容的新功能, WriteObject
請將新的 PID 指派給該類別,並將它插入封存中,前面加上 wNewClassTag 值。
接著,這個類別的描述元會使用 CRuntimeClass::Store
方法插入封存中。 CRuntimeClass::Store
會插入 類別的架構編號(如下所示),以及 類別的 ASCII 文字名稱。 請注意,使用 ASCII 文字名稱並不保證跨應用程式封存的唯一性。 因此,您應該標記資料檔案以防止損毀。 在插入類別資訊之後,封存會將 物件 放入m_pStoreMap ,然後呼叫 Serialize
方法來插入類別特定的資料。 在呼叫 Serialize
之前,將物件放入 m_pStoreMap ,可防止將物件的多個複本儲存到存放區。
返回初始呼叫端時(通常是物件的網路根目錄),您必須呼叫 CArchive::Close 。 如果您打算執行其他 CFile 作業,您必須呼叫 CArchive
Flush 方法來 防止封存損毀。
注意
此實作會對每個封存內容施加0x3FFFFFFE索引的硬性限制。 這個數位代表可儲存在單一封存中的唯一物件和類別數目上限,但單一磁片檔案可以有無限數目的封存內容。
從存放區載入物件 (CArchive::ReadObject)
載入 (擷取) 物件會使用 CArchive::ReadObject
方法,而 是 的相反方式 WriteObject
。 和 一 WriteObject
樣, ReadObject
使用者程式碼不會直接呼叫 ;使用者程式碼應該呼叫具有預期 CRuntimeClass
之 呼叫 ReadObject
的型別安全擷取運算子。 這可確保擷取作業的類型完整性。
由於實作 WriteObject
指派了增加的 PID,從 1 開始(0 預先定義為 Null 物件),因此實 ReadObject
作可以使用陣列來維護封存內容的狀態。 從存放區讀取 PID 時,如果 PID 大於m_pLoadArray 目前的上限 , ReadObject
就會知道新的物件(或類別描述)會跟著。
架構編號
當遇到 類別的 方法時 IMPLEMENT_SERIAL
,指派給 類別的架構編號是類別實作的「版本」。 架構是指 類別的實作,而不是指定物件的持續性次數(通常稱為物件版本)。
如果您想要在一段時間內維護相同類別的數個不同的實作,當您修改物件的方法實作時,遞增架構可讓您撰寫程式碼,以載入使用舊版實 Serialize
作所儲存的物件。
方法 CArchive::ReadObject
會在永續性存放區中遇到與記憶體中類別描述的架構編號不同的架構編號時,擲回 CArchiveException 。 從這個例外狀況復原並不容易。
您可以使用 VERSIONABLE_SCHEMA
結合架構版本 (位 OR ) 來防止擲回此例外狀況。 藉由使用 VERSIONABLE_SCHEMA
,您的程式碼可以藉由檢查 CArchive::GetObjectSchema 的傳回值 ,在其函式中 Serialize
採取適當的動作。
直接呼叫序列化
在許多情況下,和 ReadObject
的一般物件封存配置 WriteObject
的額外負荷並非必要。 這是將資料序列化為 CDocument 的常見案例。 在此情況下,會 Serialize
直接呼叫 的 CDocument
方法,而不是使用擷取或插入運算子。 檔的內容可能會反過來使用較一般的物件封存配置。
直接呼叫 Serialize
具有下列優點和缺點:
在序列化物件之前或之後,不會將額外的位元組新增至封存。 這不僅會使儲存的資料更小,而且可讓您實
Serialize
作可處理任何檔案格式的常式。MFC 已調整,
WriteObject
因此,除非您需要其他用途更一般的物件封存配置,否則不會將 和ReadObject
實作和相關集合連結至您的應用程式。您的程式碼不需要從舊的架構編號復原。 這可讓您的檔序列化程式碼負責編碼架構編號、檔案格式版本號碼,或您在資料檔案開頭使用的任何識別號碼。
使用 直接呼叫
Serialize
序列化的任何物件都不得使用CArchive::GetObjectSchema
,或必須處理傳回值 (UINT)-1,指出版本未知。
因為 Serialize
直接在您的檔上呼叫 ,所以檔子物件通常無法封存其父檔的參考。 這些物件必須明確提供其容器檔案的指標,或者您必須使用 CArchive::MapObject 函式,在封存這些背面指標之前,將指標對應 CDocument
至 PID。
如先前所述,您應該在直接呼叫 Serialize
時自行編碼版本和類別資訊,讓您稍後可以變更格式,同時仍維持與舊版檔案的回溯相容性。 直接序列化物件之前或呼叫基類之前,可以明確呼叫 函 CArchive::SerializeClass
式。