深入浅出MFC 第8章 关于序列化(Serialize)的一些问题

来源:互联网 发布:qq人工智能聊天机器人 编辑:程序博客网 时间:2024/05/21 02:49

    很好的一篇文章,转载自:http://blog.csdn.net/jwj070524/article/details/10263191 

   不知不觉跟随者侯捷的脚步对着电子书看了将近600页,就一个感觉:云里雾里。函数调用一层嵌一层,还有那该死的virtual关键字,每次调用时都得长个心眼,先要看看派生类有没有重写它。吐槽归吐槽,MFC设计之精巧不容质疑,其中的RTTI(Running Time Type Information),动态生成(Dynamic Create),序列化(Serialization),消息映射(Message Map),命令路由(Command Route)(最后这两个还没深入研究),我已经无法用更好的赞美之词去评价它了,至少,在了解它之前我从没见过类似的设计。

 

    好了,下面是我发现的问题以及解释。

    电子书第602页(也应该是书本的第537页,我只有电子书,所以这是电子书上显示的页码)上标题为“在CObList中加入CStroke以外的类别”这一小节,侯捷用三个类做写文件的测试,分别是CStroke,CRectangle,CCircule。生成的文件内容如下:

000000: 06 00 FF FF 01 00 07 00 43 53 74 72 6F 6B 65 02 ........CStroke.
000010: 00 02 00 6E 00 00 00 24 00 00 00 6E 00 00 00 24 ...n...$...n...$
000020: 00 00 00 FF FF 01 00 0A 00 43 52 65 63 74 61 6E .........CRectan
000030: 67 6C 65 11 00 00 00 22 00 00 00 33 00 00 00 44 gle...."...3...D
000040: 00 00 00 FF FF 01 00 07 00 43 43 69 72 63 6C 65 .........CCircle
000050: 55 00 00 00 66 00 00 00 77 00 00 00 01 80 02 00 U...f...w.......
000060: 02 00 6E 00 00 00 55 00 00 00 6E 00 00 00 55 00 ..n...U...n...U.
000070: 00 00 03 80 11 00 00 00 22 00 00 00 33 00 00 00 ........"...3...
000080: 44 00 00 00 05 80 55 00 00 00 66 00 00 00 77 00 D.....U...f...w.
000090: 00 00                                                                                 ..

图8-10a TEST.SCB 文件内容,文件全长146 个字节。

     先回顾一下,当CStroke,CRectangle,CCircule的对象第一次存入文件时CArchive的实例会自动将类的信息一并写入文件,包括FFFF标识(新的类的标识),nSchema(版本标识),类名的长度,类名,存储的内容。具体一点比如说文件中的第一个对象:FF FF 01 00 07 00 43 53 74 72 6F 6B 65 02 00 02 00 6E 00 00 00 24 00 00 00 6E 00 00 00 24 00 00 00。第一个字(WORD)FFFF表示是一个新的类,第二个字0001(注意Little Endian)表示类的索引值,第三个字0007表示类名的长度(长度是7),接着7个字节43 53 74 72 6F 6B 65就是类名了("CStroke"),然后剩下的就是实例中存储的内容,由CStroke类的Serialize函数控制,不再解释。读完之后接着又是FFFF标识,表明又是一个新的类,,,,,,(注意:文件的第一个字节是0006表明有六个实例存在文件上这是由CTypedPtrList类的Sreialize函数控制的,感觉奇怪吗?如果你仔细跟踪源代码就会明白)

 

     好了,前面三个类得的对象已经存储了,再次,我们把全新的三个类的实例按原先顺序再存储一次。由于这三个类的信息已经在先前的一次存数过程中配置好了,所以就不必再存那些多余的东西,MFC所做的就是将原先的索引值放到文件中去,代表一个已经在前面出现过的类。我现在的疑惑是:“为什么这三个索引值是8001,8003,8005而不是期望中的8001,8002,8003?”

 

      羊毛出在羊身上,百思不得其解之下我翻开MFC之中这部分的源代码,在ARCOBJ.CPP这个文件之中(我用的是VC6),具体关注以下三个函数:

void CArchive::MapObject(const CObject* pOb)

void CArchive::WriteObject(const CObject* pOb)

void CArchive::WriteClass(const CRuntimeClass* pClassRef)

      我只考虑存储的情景,读文件也是类似的操作。

      先分析MapObject函数,这个函数主要完成两个任务,来维护存放在CArchive中的一个映射(Map)。1.若Map为空,对它分配内存并初始化,把m_nMapCount置1,这点需注意。2.若pObj不空(表示它指向的实例即将要写到文件中去),则把pObj放到映射中去,key是pObj,value是(void*)m_nMapCount++。总结:一个key对应一个独一无二的value,n_MapCount在赋值之后会自动加1。

[cpp] view plaincopy
  1. void CArchive::MapObject(const CObject* pOb)  
  2. {  
  3.     if (IsStoring())  
  4.     {  
  5.         if (m_pStoreMap == NULL)  
  6.         {  
  7.             // initialize the storage map  
  8.             //  (use CMapPtrToPtr because it is used for HANDLE maps too)  
  9.             m_pStoreMap = new CMapPtrToPtr(m_nGrowSize);  
  10.             m_pStoreMap->InitHashTable(m_nHashSize);  
  11.             m_pStoreMap->SetAt(NULL, (void*)(DWORD)wNullTag);  
  12.             m_nMapCount = 1;  
  13.         }  
  14.         if (pOb != NULL)  
  15.         {  
  16.             CheckCount();  
  17.             (*m_pStoreMap)[(void*)pOb] = (void*)m_nMapCount++;  
  18.         }  
  19.     }  
  20.     else  
  21.     {  
  22.         if (m_pLoadArray == NULL)  
  23.         {  
  24.             // initialize the loaded object pointer array and set special values  
  25.             m_pLoadArray = new CPtrArray;  
  26.             m_pLoadArray->SetSize(1, m_nGrowSize);  
  27.             ASSERT(wNullTag == 0);  
  28.             m_pLoadArray->SetAt(wNullTag, NULL);  
  29.             m_nMapCount = 1;  
  30.         }  
  31.         if (pOb != NULL)  
  32.         {  
  33.             CheckCount();  
  34.             m_pLoadArray->InsertAt(m_nMapCount++, (void*)pOb);  
  35.         }  
  36.     }  
  37. }  

 

     再看WriteObject,假设pOb不为空。若pOb所指向的对象已经存到文件中去了(也就是进入第二个if子句),那么只要往文件中存储先前那个对象的索引值就可以了,这就避免了同一份数据重复存储,并且还原时能够做到彻底还原(存储时两个指针指向同一个对象,读取后两个指针依旧指向同一个对像)。若pOb指向的对象是第一次存储(进入最后的那个else),先调用WriteClass,接着在Map中给pOb加一个索引值。

     至此似乎没有什么异常,但是为什么文件中的索引值不是连续的呢?谜底就要揭开,蹊跷就在前面最后两小句中“先调用WriteClass,接着在Map中给pOb加一个索引值”。

[cpp] view plaincopy
  1. void CArchive::WriteObject(const CObject* pOb)  
  2. {  
  3.     // object can be NULL  
  4.     ASSERT(IsStoring());    // proper direction  
  5.   
  6.     DWORD nObIndex;  
  7.     ASSERT(sizeof(nObIndex) == 4);  
  8.     ASSERT(sizeof(wNullTag) == 2);  
  9.     ASSERT(sizeof(wBigObjectTag) == 2);  
  10.     ASSERT(sizeof(wNewClassTag) == 2);  
  11.   
  12.     // make sure m_pStoreMap is initialized  
  13.     MapObject(NULL);  
  14.   
  15.     if (pOb == NULL)  
  16.     {  
  17.         // save out null tag to represent NULL pointer  
  18.         *this << wNullTag;  
  19.     }  
  20.     else if ((nObIndex = (DWORD)(*m_pStoreMap)[(void*)pOb]) != 0)  
  21.         // assumes initialized to 0 map  
  22.     {  
  23.         // save out index of already stored object  
  24.         if (nObIndex < wBigObjectTag)  
  25.             *this << (WORD)nObIndex;  
  26.         else  
  27.         {  
  28.             *this << wBigObjectTag;  
  29.             *this << nObIndex;  
  30.         }  
  31.     }  
  32.     else  
  33.     {  
  34.         // write class of object first  
  35.         CRuntimeClass* pClassRef = pOb->GetRuntimeClass();  
  36.         WriteClass(pClassRef);  
  37.   
  38.         // enter in stored object table, checking for overflow  
  39.         CheckCount();  
  40.         (*m_pStoreMap)[(void*)pOb] = (void*)m_nMapCount++;  
  41.   
  42.         // cause the object to serialize itself  
  43.         ((CObject*)pOb)->Serialize(*this);  
  44.     }  
  45. }  

     我给你举个例子,你就明白了:CStroke类是CObject的派生类,我现在有一个CStroke类的实例化要写到文件中去,假设文件中什么都没有。经过重重函数嵌套调用,终于来到了CArchive中的WriteObject方法。在WriteObject中,首先初始化Map成员,并令m_nMapCount=1,然后由于Map为空,进入最后一个else中。生成pClassRef后调用WriteClass,在WriteClass中判断pClassRef是否在Map中(肯定不在,Map是空的),所以果断地把pClassRef加到Map中,并令m_nMapCount自增,此时m_nMapCount为2,pClassRef的索引为1。跳出WriteClass回到WriteObject,执行下面这句:

(*m_pStoreMap)[(void*)pOb] = (void*)m_nMapCount++;

    这时候m_nMapCount为3,pObj的索引为2。

     明白了吗,存储一个全新的类的实例,索引会自增两次!一个是CRuntimeClass指针,一个是CStroke指针。

如果以后再次存储一个新的CStroke类的实例,索引只会自增一次,因为CStroke类中的CRuntimeClass成员是静态(static)的,在程序执行过程中它的地址是不会变的。

 

     好了,根据文章开始所介绍的那个测试的例子,当所有的成员都序列化之后,Map应该就是这个样子:

指针(Map中的Key)Map中的索引(Value)          文件中的标志(Tag)&CStroke::classCStroke1
CStroke*2FFFF0001&CRectangle::classCRectangle3
CRectangle*4FFFF0001&CCircle::classCCircle5
CCircle*6FFFF0001CStroke*78001CRectangle*88003CCircle*98005

     再解释下,当存储一个新类的实例时,文件中的Tag应该是它的m_nSchema(版本号,在CRuntime::Store函数内),加上前面的FFFF表示一个新类;当存储一个旧类的实例时,文件中的Tag应该是那个类中的静态RuntimeClass成员的索引与0x8000 位运算(或)操作的值也就是0x8000 | 0001 =》 0x8001这样子(WriteClass函数中处理)。

 

     最后把WriteClass的码贴上来,看看是不是这样?写文件是这样,读文件也类似。

[cpp] view plaincopy
  1. void CArchive::WriteClass(const CRuntimeClass* pClassRef)  
  2. {  
  3.     ASSERT(pClassRef != NULL);  
  4.     ASSERT(IsStoring());    // proper direction  
  5.   
  6.     if (pClassRef->m_wSchema == 0xFFFF)  
  7.     {  
  8.         TRACE1("Warning: Cannot call WriteClass/WriteObject for %hs.\n",  
  9.             pClassRef->m_lpszClassName);  
  10.         AfxThrowNotSupportedException();  
  11.     }  
  12.   
  13.     // make sure m_pStoreMap is initialized  
  14.     MapObject(NULL);  
  15.   
  16.     // write out class id of pOb, with high bit set to indicate  
  17.     // new object follows  
  18.   
  19.     // ASSUME: initialized to 0 map  
  20.     DWORD nClassIndex;  
  21.     if ((nClassIndex = (DWORD)(*m_pStoreMap)[(void*)pClassRef]) != 0)  
  22.     {  
  23.         // previously seen class, write out the index tagged by high bit  
  24.         if (nClassIndex < wBigObjectTag)  
  25.             *this << (WORD)(wClassTag | nClassIndex);  
  26.         else  
  27.         {  
  28.             *this << wBigObjectTag;  
  29.             *this << (dwBigClassTag | nClassIndex);  
  30.         }  
  31.     }  
  32.     else  
  33.     {  
  34.         // store new class  
  35.         *this << wNewClassTag;  
  36.         pClassRef->Store(*this);  
  37.   
  38.         // store new class reference in map, checking for overflow  
  39.         CheckCount();  
  40.         (*m_pStoreMap)[(void*)pClassRef] = (void*)m_nMapCount++;  
  41.     }  
  42. }  


    如有错误,欢迎指正。

0 0
原创粉丝点击