您的位置:首页 > 其它

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

2014-01-13 10:53 417 查看
很好的一篇文章,转载自: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

void CArchive::MapObject(const CObject* pOb)

{

if (IsStoring())

{

if (m_pStoreMap == NULL)

{

// initialize the storage map

// (use CMapPtrToPtr because it is used for HANDLE maps too)

m_pStoreMap = new CMapPtrToPtr(m_nGrowSize);

m_pStoreMap->InitHashTable(m_nHashSize);

m_pStoreMap->SetAt(NULL, (void*)(DWORD)wNullTag);

m_nMapCount = 1;

}

if (pOb != NULL)

{

CheckCount();

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

}

}

else

{

if (m_pLoadArray == NULL)

{

// initialize the loaded object pointer array and set special values

m_pLoadArray = new CPtrArray;

m_pLoadArray->SetSize(1, m_nGrowSize);

ASSERT(wNullTag == 0);

m_pLoadArray->SetAt(wNullTag, NULL);

m_nMapCount = 1;

}

if (pOb != NULL)

{

CheckCount();

m_pLoadArray->InsertAt(m_nMapCount++, (void*)pOb);

}

}

}


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

[cpp] view
plaincopy

void CArchive::WriteObject(const CObject* pOb)

{

// object can be NULL

ASSERT(IsStoring()); // proper direction



DWORD nObIndex;

ASSERT(sizeof(nObIndex) == 4);

ASSERT(sizeof(wNullTag) == 2);

ASSERT(sizeof(wBigObjectTag) == 2);

ASSERT(sizeof(wNewClassTag) == 2);



// make sure m_pStoreMap is initialized

MapObject(NULL);



if (pOb == NULL)

{

// save out null tag to represent NULL pointer

*this << wNullTag;

}

else if ((nObIndex = (DWORD)(*m_pStoreMap)[(void*)pOb]) != 0)

// assumes initialized to 0 map

{

// save out index of already stored object

if (nObIndex < wBigObjectTag)

*this << (WORD)nObIndex;

else

{

*this << wBigObjectTag;

*this << nObIndex;

}

}

else

{

// write class of object first

CRuntimeClass* pClassRef = pOb->GetRuntimeClass();

WriteClass(pClassRef);



// enter in stored object table, checking for overflow

CheckCount();

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



// cause the object to serialize itself

((CObject*)pOb)->Serialize(*this);

}

}

我给你举个例子,你就明白了: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*6FFFF0001
CStroke*78001
CRectangle*88003
CCircle*98005
再解释下,当存储一个新类的实例时,文件中的Tag应该是它的m_nSchema(版本号,在CRuntime::Store函数内),加上前面的FFFF表示一个新类;当存储一个旧类的实例时,文件中的Tag应该是那个类中的静态RuntimeClass成员的索引与0x8000 位运算(或)操作的值也就是0x8000
| 0001 =》 0x8001这样子(WriteClass函数中处理)。

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

[cpp] view
plaincopy

void CArchive::WriteClass(const CRuntimeClass* pClassRef)

{

ASSERT(pClassRef != NULL);

ASSERT(IsStoring()); // proper direction



if (pClassRef->m_wSchema == 0xFFFF)

{

TRACE1("Warning: Cannot call WriteClass/WriteObject for %hs.\n",

pClassRef->m_lpszClassName);

AfxThrowNotSupportedException();

}



// make sure m_pStoreMap is initialized

MapObject(NULL);



// write out class id of pOb, with high bit set to indicate

// new object follows



// ASSUME: initialized to 0 map

DWORD nClassIndex;

if ((nClassIndex = (DWORD)(*m_pStoreMap)[(void*)pClassRef]) != 0)

{

// previously seen class, write out the index tagged by high bit

if (nClassIndex < wBigObjectTag)

*this << (WORD)(wClassTag | nClassIndex);

else

{

*this << wBigObjectTag;

*this << (dwBigClassTag | nClassIndex);

}

}

else

{

// store new class

*this << wNewClassTag;

pClassRef->Store(*this);



// store new class reference in map, checking for overflow

CheckCount();

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

}

}

如有错误,欢迎指正。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: