随笔 - 14  文章 - 1 评论 - 13 
<2024年11月>
272829303112
3456789
10111213141516
17181920212223
24252627282930
1234567

常用链接

留言簿(1)

随笔分类

随笔档案

相册

搜索

  •  

最新评论

阅读排行榜

评论排行榜

 

TN002: 持久化对象数据格式

                                                                             By zhkza99c

这篇开始发现翻译起来开始吃力了,但是慢慢来,一点一点提高。。。希望有人给我挑错。

先在这里谢谢了。

这篇笔记描述了MFC关于支持C++对象持久化和当它(C++对象)被保存在文件里时对象数据的格式。使用 DECLARE_SERIAL  和  IMPLEMENT_SERIAL  这两个宏可以做到这一点。

The Problem 问题

MFC对持久化数据的实现依赖于一种紧凑的二进制格式,这种格式可以使许多对象紧凑的连接在一起儿构成一个文件。这种二进制格式提供了一个描述数据如何存储的结构,但是其实是对象的函数 序列化(Serialize) 提供存储对象的方法。

MFC通过使用类 CArchive .来解决结构化问题。一个 CArchive 对象持久化的联系,这种上下的联系从archive被建立时开始,一直到 CArchive::Close  这个函数被调用为止,无论是程序员显示的或者隐式的调用 CArchive 的析构方法之前, CArchive 都是存在的。

这个笔记描述了 CArchive  的成员函数ReadObject WriteObject 是如何实现的。 ReadObject WriteObject  不是直接调用的,而是通过DECLARE_SERIAL  IMPLEMENT_SERIAL 宏使用特定类型的安全插入和提取操作自动生成的。

class CMyObject : public CObject 

    DECLARE_SERIAL(CMyObject) 

}; 

  IMPLEMENT_SERIAL(CMyObj, CObject, 1) 

 // 用法示例(ar 是一个CArchive&) 

CMyObject* pObj; 

CArchive& ar; 

ar << pObj;        // 调用 ar.WriteObject(pObj) 

ar >> pObj;        // 调用 ar.ReadObject(RUNTIME_CLASS(CObj)) 

这个笔记描述的代码都可以在MFC的源代码ARCOBJ.CPP中找到。这也意味着 CArchive  的实现可以在ARCCORE.CPP里找到。

存储数据到存储空间(CArchive::WriteObject)

成员函数 CArchive::WriteObject  构建出数据头用来重建对象,这个数据有两部分组成:对象的类型和对象被写出时的标识,因此无论有多少个指向该对象的指针,该对象仅仅有一个拷贝被存储。(包括循环指针).

存储(插入)和重建(提取)对象以来于数个“常量的列表”,下面是以二进制保存的值和所提供的重要的archive数据信息(注意‘w’打头意味着这些都是16位的数据(word型)):

Tag

Description

wNullTag

NULL对象指针时使用(0)

wNewClassTag

表明类的描述是一个新的archive 上下关联。(-1).

wOldClassTag

表明正在被读取的类的对象可以从上下关联找到。(0x8000).


当存储对象时,archive包含了一个 CMapPtrToPtr  ( m_pStoreMap ,它是一个从被存储对象到一个32位持久化标示符(PID)的映射。PID是分配给每一个对象和每一个类名称的独一无二的标示,它被存在archive的上下关联中。这些PID的值是从1开始的。需要注意的是这些PID在archive作用范围之外是没有任何意义的,并且特别是不要把他们同记录号码或其他标示项所搞混。

自从MFC4.0版本开始, CArchive  类已经被扩张为支持非常大的archive了。在之前的版本中,PID是16位的,限制archive的对象到0x7FFE (32766)这么大,现在PID是32位的了,但是他们除非超过了0x7FFE都是以16位写出的。大的PID是记录在 0x7FFF之后的32位的PID。这一个技术保留了向前的兼容性。

当存储一个对象到archive的要求产生时(通常是通过全局性插入操作),将会检测指向 CObject  的指针是否是NULL,如果这个指针是NULL的,那么wNullTag 这个标志位将被插入到archive流中。

如果我们有一个有效的能被序列化的对象指针(这个类是一个 DECLARE_SERIAL  的类),我们检测  m_pStoreMap  来看看这个对象是不是已经被存储过了,如果适当,那么插入一个与这个对象相联系的32位的PID。
 

如果这个对象之前没有被存储,那么有两个可能性我们必须考虑到:对于archive的上下关联来说无论是对象本身还是他明确的类型(就是说,类)都是新的,或者说这个对象是一个已经被看到的明确的类型。来确定是否这个类型已经被看到我们需要从 m_pStoreMap  查询一个 CRuntimeClass 对象,这个对象是否是我们已经存储的一个 CRuntimeClass对象。 如果我们之前看到了这个类, WriteObject  就会插入一个位标签或上wOldClassTag再与上这个索引( WriteObject  inserts a tag that is the bit-wise OR'ing of wOldClassTag and this index.)。如果这个 CRuntimeClass  对于archive是新的上下关联,那么 WriteObject  给这个类分配一个新的PID并且插入到archive中,并且在wNewClassTag 值之前。

这个类的说明会在之后使用 CRuntimeClass  成员函数  Store 插入到archive中。 CRuntimeClass::Store  插入这个类架构的号码(之前有提到)和这个类的ASCII文本名称。注意是使用ASCII文本而并不保证在archive中是独一无二的,因此一个明智的行为是标示你的数据文件来防止冲突。插入类的信息后,archive将对象置入 m_pStoreMap 并且调用Serialize成员函数来向archive插入这个类的特殊数据。在调用 Serialize  之前将对象置入 m_pStoreMap可以阻止重复拷贝这一个对象。

当返回到初始化调用时(通常是在对象网络的底部), Close  这个archive是很重要的。如果其他的 CFile  操作被实施了,必须调用 CArchive  的成员函数 Flush  。没有做这个会导致一个坏的archive的产生。

注意  每一个archive的索引数目是被强制限制在0x3FFFFFFE 之内的。这个数目象征这一个archive中最大可以保存的相互不同的对象和类的数目。

从存储空间读取数据(CArchive::ReadObject)

读取对象使用 CArchive::ReadObject  的成员函数,它是 WriteObject .的逆过程, ReadObject  不是被每个用户代码直接调用的。用户代码需要调用类型安全的读取操作,这个操作调用所期待的 CRuntimeClass . ReadObject  。这确定了读取操作的完整性。

WriteObject 的行为分配可以增长的PID,这个PID是从1开始的(0是被NULL对象所预先定义的),ReadObject  的行为可以使用数组来包含archive上下关联的状态。当一个PID从存储控件读取时,如果这个PID超过了当前 m_pLoadArrayd 上限,那么ReadObject 就会知道这是一个新的对象(或者新的类的描述)。

Schema号码

当碰到 IMPLEMENT_SERIAL  时Schema号码被分配给类,表示这个类的实现的版本号码。Schema涉及到类的实现,而不是所给持久化类出现的次数(通常同对象的版本关联)。

如果你尝试着包含同一个类的数个不同的实现,如同你修正你的对象的Serialize 成员函数的实现一样增长你的schema会让你写下的代码可以读取之前存储的旧的版本的实现。

CArchive::ReadObject  成员函数当遇到和内存中存储的schema号码与持久化存储对象的schema号码不一致时会抛出一个 CArchiveException 想要修复这个异常并不简单。

你可以使用 VERSIONABLE_SCHEMA  与你的schema版本来保证这个异常不被抛出,你的代码能采取适当的行为在它的 Serialize  函数中,这种行为是通过检查 CArchive::GetObjectSchema .的返回值来实现的。


直接调用序列化(Serialize)

当通常的对象的辅助操作WriteObject ReadObject 并不是被需要时,archive scheme有很多情况。通常的情况是将数据序列化放入 CDocument 这种情况下 CDocument   Serialize  成员函数被直接调用了,但并不是通过插入或者取出操作。该文档的内容可能反过来使用更普遍的对象arvhive scheme。

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

   没有额外的字节在archive之前或者在对象被序列化过程中加入,这不仅仅将存储的数据变小了,而且允许你通过执行 Serialize  的规则来操作更多的文件格式。

      MFC是协调的,因此 WriteObject和ReadObject  实现和相关的收集不会被你的应用程序连接。除非你因为一些且他的目的需要更普遍的对象archive scheme。

   你的代码不需要重写旧的schema号码。这使你的文档序列化代码有义务的为schema numbers编码,文件格式版本号码或者任何梦幻般的号码都要在你的数据文件的开始得到描述。

   任何通过直接调用Serialize序列化的对象不能使用CArchive::GetObjectSchema或者必须控制一个返回值(NULL)-1,指出未知的版本。

因为  Serialize  在你的文档里被直接调用,通常不可能为这个文档的子对象文档化来参照他们的母文档。这些对象必须显式的给一个指向他们文档容器,或者你必须在他们之后的指针被文档化前使用 CArchive::MapObject  函数来应声这个CDocument 指针到一个PID

就像笔记前述的,当直接调用串行化时你应该自己为版本号和类的信息编码,允许你之后改变格式,却仍然保持同旧文件的兼容性。CArchive::SerializeClassRef函数在直接串行化一个对象或者调用一个基类之前应该被显式的调用。

posted on 2008-03-15 03:06 田园的拾荒者 阅读(807) 评论(2)  编辑 收藏 引用 所属分类: 翻译

FeedBack:
# re: TN002: 持久化对象数据格式 2008-03-18 15:18 Sure
当里个当~好!  回复  更多评论
  
# re: TN002: 持久化对象数据格式[未登录] 2008-08-22 14:05 老虎
本来是看不懂的,不过碰巧今天看了carchive源码,
所以知道这篇东东说的是什么。
英文原文就写得比较晦涩,
所以翻译时候如果不加入译者自己的理解,
而是直接在英文原文基础上面翻译的话,
的确是很难看懂。  回复  更多评论
  
只有注册用户登录后才能发表评论。