依旧的博客

技术学习

C++博客 首页 新随笔 联系 聚合 管理
  17 Posts :: 1 Stories :: 2 Comments :: 0 Trackbacks

我们知道MFC的作用在于封装Windows的编程接口,并提供应用程序框架的开发模式。为了完成从前者到后者的过渡,MFC实现了几种基本机制,它们是消息映射,命令传递,运行时类信息(RTCI),动态创建和序列化。

消息映射和命令传递是对SDK程序交互机制的封装。序列化是应用程序需要的一种基本特性,即把数据保存到磁盘和从磁盘打开数据。通过RTCI和动态创建,可以把软件的对象数据保存到磁盘,反过来从这些数据识别和恢复对象,从而实现对象的序列化。基于数据库的序列化机制和这种方式不同,应用程序和数据库之间有一个约定,以什么样的格式保存什么样的数据,再以同样的方式打开,并且如何重建对象数据也定下来了,在打开数据时,应用程序不需要有适应性,不需要识别数据类型,也不需要根据在运行期才确定的类型名称创建其对象。

动态创建就是创建某种类型的对象,具体类型在运行时确定,编译时可能不知道。比如运行时用户输入一个类型名称,如果该类型是程序类型体系中的一员,则程序中将能够创建该类型的对象。下面的代码是使用MFC动态创建机制的一个简化的例子:

CRuntimeClass* g_pFirstClass;
void func()
{
     char szClassName[64];
     CRuntimeClass* pClass;
     CObject* pObject;
    
     cout << "enter a class name...  ";
     cin >> szClassName;
    
     for (pClass = g_pFirstClass; pClass != NULL; pClass = pClass->m_pNextClass)
     {
          if (strcmp(szClassName, pClass->m_lpszClassName) == 0)
              pObject = pClass->CreateObject();
     }
}

实现动态创建的思路是把动态的类型名称与程序类型体系中的每一个进行比较,与某个类型吻合时让该类型创建自身的对象。这样,支持动态创建的类库中的每一个类都要额外实现一些功能,即判别一个名称是否与自身相符,以及创建自身的对象。

判别一个名称是否与自身相符,这是运行时类识别的内容,所以MFC动态创建是在RTCI基础上实现的。

RTCI是一个对象能够判定自己是否属于某种类型,该类型的名称在运行时确定,编译时可能不知道。从下面的例子很容易理解RTCI,

void Func()
{
     char szClassName[64];
     CDocument* pDoc = new CDocument;
    
     cout << "enter a class name...  ";
     cin >> szClassName;
    
     cout << pDoc->IsKindOf(szClassName); //是返回1,否返回0
}

有一点需要说明的是,因为CDocument派生于CObject,所以IsKindOf对于CObject也要返回1。因为我们是从动态创建出发的,所以如果是这样可能会有一点背离初衷。但是RTCI明显和动态创建有密切联系,RTCI也可能有单独的价值,所以先把RTCI实现起来。

实现RTCI的思路是让每一个类记录自身的类型信息,并提供IsKindOf(char*)函数进行所给类型与自身类型的比较,而且还要能访问基类的类型信息,进行比较,一直到根类。所以记录的类型信息要按继承关系连起来,每个类的IsKindOf()还要调用基类的IsKindOf()。MFC把要记录的类型信息抽取到一个CRuntimeClass结构体中,每个类中加入一个CRuntimeClass成员即可。

现在回到动态创建,在RTCI建立的数据结构基础上将可实现它。动态创建从不同于IsKindOf()的角度使用这一数据结构,它要遍历所有类型的CRuntimeClass。那么仅仅有继承关系的类的CRuntimeClass相连还不够,要把所有类的CRuntimeClass连成一个链表。其实动态创建并不关心类间的继承关系,它平等看待每个类。现在以CRuntimeClass为结点构成一个纵横两个方向的链表,IsKindOf()和动态创建分别使用它不同的侧面。

序列化的概念是在文件中存储对象信息,并能根据它恢复对象。对于文档视图结构的软件,用户需要保存所编辑的文档和打开已编辑的文档,这正是序列化的应用,所以序列化是非常重要的一种特性。在序列化恢复对象时,就可以用到动态创建。

使用MFC序列化的例子如下,

void CMyDocument::Serialize(CArichive &ar)
{
    if (ar.IsStoring())
    {
        ar << m_pMyClass; //CMyClass m_pMyClass;
    }
    else
    {
        ar >> m_pMyClass;
    }
}

一个支持序列化的类提供Serialize(CArchive &)函数,重载<<和>>操作。注意两者是不同的,在上例中,CMyDocument类的信息并不被序列化,而CMyClass类的信息被序列化。实际上一个序列化类的<<和>>操作,其不涉及类信息的部分是调用Serialize()完成的,它必须同时实现这两者。

按照MFC的要求,需要在支持序列化的类定义中使用DECLARE_SERIAL宏,在类实现中使用IMPLEMENT_SERIAL宏。我们看一下这两个宏实现了什么,

#define DECLARE_SERIAL(class_name) \
 _DECLARE_DYNCREATE(class_name) \
 AFX_API friend CArchive& AFXAPI operator>>(CArchive& ar, class_name* &pOb);

#define IMPLEMENT_SERIAL(class_name, base_class_name, wSchema) \
 CObject* PASCAL class_name::CreateObject() \
  { return new class_name; } \
 _IMPLEMENT_RUNTIMECLASS(class_name, base_class_name, wSchema, \
  class_name::CreateObject) \
 AFX_CLASSINIT _init_##class_name(RUNTIME_CLASS(class_name)); \
 CArchive& AFXAPI operator>>(CArchive& ar, class_name* &pOb) \
  { pOb = (class_name*) ar.ReadObject(RUNTIME_CLASS(class_name)); \
   return ar; } \

主要是加入了对>>的重载,但是没有重载<<,MFC仅提供了CObject对<<的重载,如下,

_AFX_INLINE CArchive& AFXAPI operator<<(CArchive& ar, const CObject* pOb)
 { ar.WriteObject(pOb); return ar; }

这是因为在序列化读和写的时候,都需要具体类的CRuntimeClass信息。相应的GetRuntimeClass()是一个虚函数,CObject重载<<,在写类信息时调用到该函数,由于虚函数机制,写入的是具体类的信息。但是这里隐含着一个条件,就是调用<<和GetRuntimeClass()时,具体类对象已经存在了,而调用>>和读入类信息时,该类对象还未被创建,所以无法利用这种机制,只能在每个具体类中都重载一次>>。我觉得《深入解析MFC》对这个问题的解释不正确。

这里有一个问题需要明确一下,序列化为什么要写入类信息?一是它应该保存完整的能够独立恢复对象的信息,二是在程序读入对象时,要把它的类信息和程序中期望的(所能处理的)类信息相比较,进行检验。

看IMPLEMENT_SERIAL宏对重载>>的实现,是提供一个期望的CRuntimeClass结构(用于检验),委托CArchive进行对象读取。因为读对象时首先要跟文件打交道,所以交给CArchive处理,随后把读出的数据写入对象时,CArchive再调用具体类的Serialize(),如此合作是十分恰当的。在这里,CArchive还负责了读出和检验类信息,然后创建对象的过程。因为一方面具体类对象还不存在,另一方面这些操作对所有具体类都没有分别,应该提出来,在类级别实现或者让合作者实现。实际上,MFC先把这个过程交给CArchive::ReadClass(),后者又调用CRuntimeClass::Load()。 

对于序列化来说,搞清它的概念以后,就是实现Serialzie(),重载<<和>>。对<<和>>的重载涉及很多工作,MFC已经帮我们实现了,我们也看见了大概的设计,主要是与CArchive分工合作,其次是CRuntimeClass。

现在看到CRuntimeClass结构体在MFC对RTCI,动态创建和序列化的实现中都起着重要的作用,重新认识一下这个数据结构很有必要。

CRuntimeClass包含了关于类的各种信息和有关操作。把类及其基类的CRuntimeClass连成一个链表,就可以很方便地实现RTCI的IsKindOf();把所有类的CRuntimeClass连成一个链表,再加上一个简单的CreateObject函数,就可以对以任意类名进行动态创建的企图做出反应;CRuntimeClass还实现了向文件读写类信息的Load(),Store(),配合序列化的实现。

在分析消息映射和命令传递机制之前,需要对Windows程序模型有很好的理解。

未完待续...


参考:

《深入解析MFC》/中国电力出版社
《深入浅出MFC》/华中科大出版社
《Windows程序设计》/北大出版社

posted on 2006-05-15 19:33 依旧的博客 阅读(1125) 评论(0)  编辑 收藏 引用 所属分类: 编程

只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   博问   Chat2DB   管理