TN002:持久对象数据格式

此说明介绍支持持久C++对象的 MFC 例程,以及存储在文件中时对象数据的格式。 这仅适用于具有 DECLARE_SERIALIMPLEMENT_SERIAL 宏的类。

问题

持久数据存储的 MFC 实现将多个对象的数据存储在文件的单个连续部分。 对象的 Serialize 方法将对象的数据转换为压缩的二进制格式。

该实现通过使用 CArchive 类保证所有数据都以相同的格式保存。 它使用对象 CArchive 作为翻译器。 此对象从创建到调用 CArchive::Close 的时间一直保留。 当程序退出包含 CArchive 的作用域时,程序员可以显式调用此方法,也可以由析构函数隐式调用。

此说明介绍了 CArchive 成员 CArchive::ReadObjectCArchive::WriteObject 的实现。 可以在 Arcobj.cpp 中找到这些函数的代码,以及Arccore.cpp中的主要实现 CArchive 。 用户代码不直接调用ReadObjectWriteObject。 这些对象由专用于某类的类型安全插入和提取运算符使用,这些运算符由DECLARE_SERIAL和IMPLEMENT_SERIAL宏自动生成。 以下代码演示如何 WriteObjectReadObject 隐式调用:

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 位数量):

标记 DESCRIPTION
wNullTag 用于 NULL 对象指针(0)。
wNewClassTag 指明以下类描述在此存档上下文(-1)中是新的内容。
wOldClassTag 指示在此上下文(0x8000)中已看到被读取对象的类别。

存储对象时,存档会维护一个 CMapPtrToPtrm_pStoreMap),它将存储对象映射为32位永久标识符(PID)。 PID 分配给存档上下文中保存的每个唯一对象和每个唯一类名称。 从 1 开始按顺序分发这些 PID。 这些 PID 在存档范围之外没有意义,特别是不会与记录编号或其他标识项混淆。

在类中 CArchive ,PID 是 32 位,但它们被写为 16 位,除非它们大于0x7FFE。 大型 PID 表示为 0x7FFF 后接 32 位 PID。 这与在早期版本中创建的项目保持兼容性。

发出将对象保存到存档的请求时(通常通过使用全局插入运算符),则会对 NULL CObject 指针进行检查。 如果指针为 NULL,则会将 wNullTag 插入存档流中。

如果指针不为 NULL 且可序列化(类为 DECLARE_SERIAL 类),则代码将检查 m_pStoreMap 以查看对象是否已保存。 如果有,代码会将与该对象关联的 32 位 PID 插入到存档流中。

如果对象之前尚未保存,有两种可能性:其一,对象和对象的确切类型(即类)都是此存档上下文中的新对象;其二,对象的确切类型已经在此上下文中出现过。 为了确定是否已经识别过该类型,代码会在m_pStoreMap中查询一个与所保存对象关联的对象匹配的CRuntimeClass对象。 如果存在匹配项,WriteObject则插入一个标记,该标记是 OR 的按位和此索引。 CRuntimeClass如果在此存档上下文中是新的,WriteObject会为该类分配一个新的 PID,并将其插入存档中,前面是 wNewClassTag 值。

然后,通过CRuntimeClass::Store方法将该类的描述符插入到存档中。 CRuntimeClass::Store 插入类的架构编号(请参阅下文)和类的 ASCII 文本名称。 请注意,使用 ASCII 文本名称不能保证跨应用程序存档的唯一性。 因此,应标记数据文件以防止损坏。 在插入类信息后,存档会将对象放入 m_pStoreMap ,然后调用 Serialize 方法以插入特定于类的数据。 在调用之前将对象放入Serialize可防止将对象的多个副本保存到存储区。

返回到初始调用方(通常是对象的网络的根目录)时,必须调用 CArchive::Close。 如果计划执行其他 CFile 作,则必须调用 CArchiveFlush 方法来防止存档损坏。

注释

此实现对每个存档上下文0x3FFFFFFE索引施加硬性限制。 此数字表示可保存在单个存档中的唯一对象和类的最大数目,但单个磁盘文件可以具有无限数量的存档上下文。

从应用商店加载对象 (CArchive::ReadObject)

加载(提取)对象使用 CArchive::ReadObject 方法,与 WriteObject 相反。 与WriteObject一样,ReadObject不是由用户代码直接调用;用户代码应该调用类型安全的提取运算符,该运算符会根据预期的ReadObject调用CRuntimeClass。 这可确保提取作的类型完整性。

WriteObject由于实现中分配的 PID 从1开始增加(其中0是预定义为NULL对象),ReadObject实现可以使用数组来保留存档上下文的状态。 从存储区读取 PID 时,如果 PID 大于 m_pLoadArray的当前上限, 则说明紧跟着会有新对象(或类说明)。

架构编号

当遇到类的方法 IMPLEMENT_SERIAL 时,分配给类的架构号就是类实现的“版本”。 架构是指类的实现,而不是给定对象的持久化次数(通常称为对象版本)。

如果打算随时间推移维护同一类的多个不同实现,在修改对象 Serialize 的方法实现时递增架构将使你能够编写代码,该代码可以使用较旧版本的实现来加载存储的对象。

CArchive::ReadObject 该方法在持久存储中遇到架构编号时,该方法将引发 CArchiveException ,该架构编号与内存中类说明的架构编号不同。 要从这个异常中恢复并不容易。

可以使用 VERSIONABLE_SCHEMA 与你的模式版本(按位 OR)结合来防止引发此异常。 通过使用VERSIONABLE_SCHEMA,代码可以通过检查Serialize的返回值,在其函数中采取适当的措施。

直接调用序列化

在许多情况下,WriteObjectReadObject 的通用对象存档方案的开销并没有必要。 这是将数据序列化为 CDocument 的常见情况。 在这种情况下,直接调用 SerializeCDocument 方法,而不是使用提取或插入运算符。 文档的内容可能反过来使用更常规的对象存档方案。

直接调用 Serialize 具有以下优点和缺点:

  • 在序列化对象之前或之后,不会向存档添加额外的字节。 这不仅使保存的数据更小,还允许实现 Serialize 可以处理任何文件格式的例程。

  • MFC 经过优化,WriteObjectReadObject 的实现和相关集合不会链接到您的应用程序中,除非出于其他目的需要更通用的对象存档方案。

  • 代码不必从旧架构编号中恢复。 这使得文档序列化代码负责编码架构编号、文件格式版本号或任何在数据文件开始时使用的标识号。

  • 使用直接调用 Serialize 序列化的任何对象不得使用 CArchive::GetObjectSchema ,或者必须处理返回值 (UINT)-1,指示版本未知。

由于 Serialize 直接在文档中调用,因此文档的子对象通常无法存档对其父文档的引用。 必须显式为这些对象提供指向其容器文档的指针,或者必须使用 CArchive::MapObject 函数将这些指针映射到 CDocument PID,然后再存档这些后退指针。

如前所述,在直接调用 Serialize 时,应自行对版本和类信息进行编码,以便稍后更改格式,同时仍保持与旧文件的向后兼容性。 CArchive::SerializeClass可以在直接序列化对象之前或调用基类之前显式调用该函数。

另请参阅

按编号列出的技术说明
按类别列出的技术说明