TN002:持久的对象数据格式
此说明描述支持持续的 C++ 对象和对象数据的格式存储在文件中的 MFC 例程。 这仅适用于具有类DECLARE_SERIAL和IMPLEMENT_SERIAL宏。
问题
MFC 实现持久性数据的单个连续的文件部分中存储数据的许多对象。 该对象的Serialize方法将对象的数据转换为压缩的二进制格式。
实现可保证相同的格式保存所有数据是通过使用CArchive 类。 它使用CArchive翻译的对象。 此对象仍然存在,则会创建在您调用之前的时间从CArchive::Close。 此方法时,可以调用由程序员显式或隐式的析构函数退出程序的范围包含的CArchive。
本文描述的实现CArchive成员CArchive::ReadObject和CArchive::WriteObject。 您将找到代码中 Arcobj.cpp 和主实施这些函数CArchive Arccore.cpp 中。 用户代码不会调用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 位) 的重要信息:
Tag |
说明 |
---|---|
wNullTag |
使用空值的对象指针 (0)。 |
wNewClassTag |
指示此存档上下文 (-1) 的新类之后的说明。 |
wOldClassTag |
指示正在读取对象的类,出现在此上下文中 (0x8000)。 |
归档文件时存储对象,维护 CMapPtrToPtr ( m_pStoreMap) 是一个 32 位的持久标识符 (PID) 映射存储对象。 PID 指派给每个唯一的对象并保存归档文件的上下文中的每个唯一类名。 分发这些 Pid 按顺序从 1 开始。 这些 Pid 归档文件的范围之外没有任何意义,特别是,将不与记录号或其他标识项目混淆。
在CArchive类,Pid 是 32 位,但他们写出为 16 位除非它们是大于 0x7FFE。 大 Pid 表示为 32 位 PID 后跟 0x7FFF。 这将保持与早期版本中创建的项目的兼容性。
当请求保存到存档中的对象 (通常通过使用全局插入运算符) 时,为空值进行检查 CObject 指针。 如果指针为 NULL, wNullTag被插入到存档流。
如果不是 NULL 指针,并可序列化 (此类是DECLARE_SERIAL类),代码检查m_pStoreMap以查看是否已保存的对象。 如果是,代码会插入到存档流与该对象关联的 32 位 PID。
如果对象以前未保存过,有两种可能性考虑: 对象和对象的确切类型 (即,类) 都不熟悉此存档环境中,或者对象是已看到对精确类型。 要确定是否出现类型,代码查询m_pStoreMap的 CRuntimeClass 匹配的对象CRuntimeClass与所保存对象相关联的对象。 如果没有匹配项, WriteObject插入标记的逐位OR的wOldClassTag和该索引。 如果CRuntimeClass是新存档此上下文中, WriteObject新 PID 为该类并将其插入到存档,跟在wNewClassTag的值。
此类的描述符插入到存档使用CRuntimeClass::Store方法。 CRuntimeClass::Store插入架构编号 (见下文) 的类和类的 ASCII 文本名称。 请注意使用 ASCII 文本名称并不保证跨应用程序归档的唯一性。 因此,您应该防止损坏您的数据文件添加标签。 以下类信息的插入,存档将对象m_pStoreMap ,然后调用Serialize插入特定于类的数据的方法。 放置到m_pStoreMap在调用之前Serialize防止该对象的多个副本将保存到存储区。
当返回到初始的调用方 (通常根对象的网络),您必须调用CArchive::Close。 如果您打算执行其他 CFile操作,您必须调用CArchive方法刷新以防止损坏归档文件。
备注
此实现实施硬限制为每个存档上下文 0x3FFFFFFE 索引。此数字最大数字表示的唯一对象和类,可以将保存在单独的存档,但单个磁盘文件可以有任意的数量的存档上下文。
从存储区 (CArchive::ReadObject) 加载对象
正在加载 (提取) 对象使用CArchive::ReadObject方法,是相反的WriteObject。 与WriteObject, ReadObject不直接调用用户代码。 用户代码应调用类型安全提取运算符调用ReadObject与预期CRuntimeClass。 这确保了提取操作的类型的完整性。
由于WriteObject实施分配不断增加的 Pid,从 1 开始的 (0 预定义的 NULL 对象), ReadObject实现可以使用数组来维护存档环境的状态。 当 PID 从存储中读取,如果 PID 大于当前上限的m_pLoadArray, ReadObject知道新的对象 (或类说明) 后面。
架构编号
分配给类的架构编号时IMPLEMENT_SERIAL类的方法遇到、 是类实现的"版本"。 架构引用类的实现,不到的次数给定的对象进行永久 (通常称为对象版本)。
如果您要维护同一类的多个不同的实现,随着时间的推移,递增架构,您可以修改对象的Serialize方法实现将使您能够编写代码,可以加载存储的使用实现的较早版本的对象。
CArchive::ReadObject方法将引发 CArchiveException 时遇到架构编号不同于类说明在内存中的架构编号的持久存储区中。 它并不容易从该异常中恢复。
您可以使用VERSIONABLE_SCHEMA结合 (按位OR) 您的架构版本,将引发此异常。 通过使用VERSIONABLE_SCHEMA,您的代码可以采取相应的措施,其Serialize函数通过检查返回值CArchive::GetObjectSchema。
电话直接进行序列化
在许多情况下的常规对象存档方案的系统开销WriteObject和ReadObject不是必需的。 这是常见的情况的序列化到数据 CDocument。 在这种情况下, Serialize方法的CDocument直接调用,不能与提取或插入运算符。 文档的内容又可能会使用更常规的对象存档方案。
调用Serialize直接具有下列优点和缺点:
没有额外的字节添加到存档之前或之后序列化对象。 这不仅使较小的、 已保存的数据,但可以实现Serialize例程,可以处理任何文件格式。
MFC 进行调整以便WriteObject和ReadObject实现和相关的集合将不会链接到您的应用程序不需要的其他原因更常规的对象存档方案。
您的代码没有恢复旧架构编号。 这使您的文档序列化代码负责编码架构编号、 文件格式的版本号,或任何标识编号您使用您的数据文件的开头。
序列化的直接调用与任何对象Serialize不能使用CArchive::GetObjectSchema或句柄返回值为-1 (UINT) 必须指示未知的版本。
因为Serialize调用直接在文档中,不通常可能要存档到其父文档的引用的文档的子对象。 这些对象必须具有一个指针到其容器文档显式或必须使用CArchive::MapObject映射函数CDocument PID 之前存档这些后指针指向的指针。
前面已提到,应进行编码的版本和类信息自己在调用Serialize直接,从而使您可以稍后更改格式,同时保持向后兼容使用较旧的文件。 CArchive::SerializeClass之前直接序列化对象,或在调用基类之前可以显式调用函数。