此说明介绍支持持久C++对象的 MFC 例程,以及存储在文件中时对象数据的格式。 这仅适用于具有 DECLARE_SERIAL 和 IMPLEMENT_SERIAL 宏的类。
问题
持久数据存储的 MFC 实现将多个对象的数据存储在文件的单个连续部分。 对象的 Serialize
方法将对象的数据转换为压缩的二进制格式。
该实现通过使用 CArchive 类保证所有数据都以相同的格式保存。 它使用对象 CArchive
作为翻译器。 此对象从创建到调用 CArchive::Close 的时间一直保留。 当程序退出包含 CArchive
的作用域时,程序员可以显式调用此方法,也可以由析构函数隐式调用。
此说明介绍了 CArchive
成员 CArchive::ReadObject 和 CArchive::WriteObject 的实现。 可以在 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 位数量):
标记 | DESCRIPTION |
---|---|
wNullTag | 用于 NULL 对象指针(0)。 |
wNewClassTag | 指明以下类描述在此存档上下文(-1)中是新的内容。 |
wOldClassTag | 指示在此上下文(0x8000)中已看到被读取对象的类别。 |
存储对象时,存档会维护一个 CMapPtrToPtr(m_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 作,则必须调用 CArchive
Flush 方法来防止存档损坏。
注释
此实现对每个存档上下文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
的返回值,在其函数中采取适当的措施。
直接调用序列化
在许多情况下,WriteObject
和 ReadObject
的通用对象存档方案的开销并没有必要。 这是将数据序列化为 CDocument 的常见情况。 在这种情况下,直接调用 Serialize
的 CDocument
方法,而不是使用提取或插入运算符。 文档的内容可能反过来使用更常规的对象存档方案。
直接调用 Serialize
具有以下优点和缺点:
在序列化对象之前或之后,不会向存档添加额外的字节。 这不仅使保存的数据更小,还允许实现
Serialize
可以处理任何文件格式的例程。MFC 经过优化,
WriteObject
和ReadObject
的实现和相关集合不会链接到您的应用程序中,除非出于其他目的需要更通用的对象存档方案。代码不必从旧架构编号中恢复。 这使得文档序列化代码负责编码架构编号、文件格式版本号或任何在数据文件开始时使用的标识号。
使用直接调用
Serialize
序列化的任何对象不得使用CArchive::GetObjectSchema
,或者必须处理返回值 (UINT)-1,指示版本未知。
由于 Serialize
直接在文档中调用,因此文档的子对象通常无法存档对其父文档的引用。 必须显式为这些对象提供指向其容器文档的指针,或者必须使用 CArchive::MapObject 函数将这些指针映射到 CDocument
PID,然后再存档这些后退指针。
如前所述,在直接调用 Serialize
时,应自行对版本和类信息进行编码,以便稍后更改格式,同时仍保持与旧文件的向后兼容性。
CArchive::SerializeClass
可以在直接序列化对象之前或调用基类之前显式调用该函数。