编程中模型
海风
在学习新知识时,我个人比较喜欢用联想、比较和总结的方法去思考问题,解决问题,使一切未知的与已知的相联系,使一切已知的相似的相比较,从而总结他们的共性,整理与理清脑中乱糟糟的知识,从而达到提升。学习编程也不例外,在学编程过程中,我发现编程技术中有一种非常非常常用的技术:模型!消息机制、文档视图结构、动态生成以及COM都使用了一种相似的模型方法去解决问题。以下请听小弟一一分析学习过程中的心得总结:
消息机制:
Windows程序的进行是依靠外部发生的事件来驱动,操作系统中有一个USER模块专门用来捕捉外围设备所发生的事件;比如当按下鼠标时,USER模块就捕捉到一个鼠标消息,而且依据当时系统界面的情况确定响应此消息的窗口,填充消息结构MSG,并将这个鼠标消息放入系统消息队列中,GetMessage函数从消息队列中取得一个消息,并依据消息的内容把消息发往特定的窗口函数,由窗口函数负责处理。每一条消息,都对应着一组代码段,程序通过执行代码段来完成我们的任务;在MFC中,用类的数据成员封装了我们要操作的数据,用函数成员封装了要执行的代码段;于是一条消息通常都对应一个函数。那么,消息怎么知道自己该由那一个类中的那一个函数成员来执行呢?
首先,MFC为每一个能处理消息的类设计了一个消息映射表Message MAP,消息映射表负责把这个类能处理的消息和其处理函数“绑”在一块。其次,我们把有继承关系的类的消息映射表链接起来形成一张消息网,通过依次比较有继承关系的各个类中映射表中的内容(MessageID)寻找与消息对应的处理函数,如果找到就调用其处理函数。如果在这张消息网中找不到相等的MessageID(网中的类中都没有定义响应这个消息的函数),即分两种情况处理:如果是命令消息或由其它控件发送过来的消息,就跳到另外一张消息网看看有没有符合的选择,跳完了专家们设定的该跳的网后仍然找不到符合的消息,则调用系统默认的处理消息的函数,找到了当然调用其处理函数了;如果是Windows消息,则不用跳了,调用系统默认处理函数就行了。由此可见,在消息传递的过程中,我们需要知道一些信息:这个消息是什么类型以及如果要“跳网”时应该先跳到那一张网,跳到那一张网后仍然找不到后才放弃。因此,在有处理消息能力的类的函数中定义了一个名为OnWndMsg函数分辨消息的类型,定义了一个名为OnCmdMsg函数来决定跳网的路线(这条路线由专家们事先设定好了,无需我们劳神)。
刚才我们说,把消息与其处理函数“绑”在一块,把有继承关系的类的消息映射网链接起来,在MFC中我们是怎么办到的呢?MFC中设定了几个宏来负责有关工作:DECLARE_MESSAGE_MAP/BEGIN_MESSAGE_MAP/ON_COMMAND/END_MESSAGE_MAP。基中DECLARE_MESSAGE_MAP这个宏里主要包含了一个消息映射表结构成员,一个获取消息映射表结构地址的函数及一个messageEntris成员。而BEGIN_MESSAGE_MAP/ON_COMMAND/ END_MESSAGE_MAP三个宏负责填充消息映射表的内容并依类的继承关系建立一张消息网。于是,当我们向类中加入一个新消息,我们就不必在类申明中加入任何的东西了,因为DECLARE_MESSAGE_MAP里有一个获取消息映射表结构地址的函数,我们就只需要向这个地址加入新的消息项和其处理函数名就行了。为类定义一个新消息就变成了做填空题。我把这种消息与函数间的结构理解成一种模型;消息与函数之间原本应该直接对应的,现在我们为了方便管理方便操作,人为地把它们分开,加入中间层(消息映射表,专门管理消息与处理函数的对应关系),以达到更强大的功能。这是一模型,间接模型,也是一种方法,即把A—C模型变成A—B—C模型。我也把做这种填空题看成一种模板,就好比画龙点睛,龙已经画好,我们只需点睛,龙就会飞了。以后我们会发现,在计算机技术中,这是一种常用的解决问题的方法。
动态生成与类型识别:
无论是执行期类型识别,动态生成还是档案的读写,都是用同一数据结构CRuntimeClass同一张称为类别型录网的网。类别型录网是由CRuntimeClass链接而成的,在类中的申明由DECLARE_DYNAMIC宏封装,链接工作由类外的IMPLEMENT_DYNAMIC宏封装完成,当然根据不同的应用,CRuntimClass有不同的内容,所用的宏的名字也略有不同。他的工作原理和消息机制有点相似,都是运用“指针”这一大特性完成桥梁工作,也都利用指针在类外另构一张网,集中管理信息。这张网有什么大用途呢?是这样的,当我们在执行期要生成一个类对象时(比如在读档案时,里面的数据要根据实际情况产生相应的对象我们才能读),我们需要的信息:类的名字,类的大小,类的建构函数,类与其它类的关系等等有关类的信息,都将记录在类别型录网上;那么当我们在执行期获得一个(用字符串表示的)类名时,我们就可以在这张网上找出对应的元素(通过比较网中成员的类名),然后调用其建构函数(里面有个指针指向其建构函数),产生出对象。当然啦,执行期所产生的对象相应的类必需要在类别型录网中才行,而这张网是由你一手一脚用IMPLEMENT_DYNAMIC建立起来的,里面有些什么类,你一清二楚。至于在如何建网,比较麻烦,反正有那两个宏自动帮我们完成这些工作,我们只需要做做填空题,确定把那些类挂在网上即可。大家回头想想,产生对象这件事,原本很简单的,原本我们可以直接产生的,可是为了把程序活跃起来,为了在执行期也能动态生成(那时我们可不能像以前那随随便便加个语句就生成一个对象了,因为我们不可能再加语句到已经分发的软件上去),于是我们加了个中间层,我们生成了一张网来集中管理类型信息,这种解决问题的办法是不是很高明!?我把动态生成的过程理解成一种模型,间接模型,也是一种方法,即把A—C模型变成A—B—C模型。我也把做这种填空题看成一种模板,就好比画龙点睛,龙已经画好,我们只需点睛,龙就会飞了。这一切都是为了为程序员提供了方便。
文档/视图结构:
我们把文档看成一种数据,而视图就是把这些数据按照我们想要的方式展示给我们看的窗口。如果一份数据只以一种方式显示,如果我们只需显示一种文件类型中的数据,那么,数据与视图一一对应也不需要玩什么花招就能满足我们的要求的。可是更多的时候,我们需要把数据以不同的方式(比如有关统计的数据,我们然望有线形图,柱形图,文字等多种方式显示给我们看)显示出来,我们更希望在同一程序中能处理数种不同文件类型中的数据并为之提供不同的显示界面,那么一一对应好像就变得行不通了!怎么办?呵呵,我们加多一个中间层就行了!这个中间层在MFC中叫作Document Template,由它来负责集中管理文档视图之间的转换,嗯,应该说是管理Document/View/Frame,Frame是包在View外面的窗口,他可以为View/Document提供专用的菜单。为什么数据能以不多的形式显示呢?那是因为,据然数据能以一种形式显示,就必定能以多种形式显示!呵呵,好像有点强辞夺理,其实呢,因为有了个中间层把文档视图分开,那么只要你按原数据多设计几个显示形式就行了,你需要什么形式显示,只要告诉Document Template,它就会体贴地帮你安排好(接上你想要的视图形式,和此视图专用的Frame)要做的工作了。同理,在同一主框架中为不同文件类型中的数据提供专用的View和Frame也由 Document Template这个中界帮我们安排。理所当然的,在Document/View/Frame三位一体结构中,有一个链表把文件类型(一个Document Template对应管理一种类型)连接起来,在显示文件中数据时也有个查找过程,即先找到要显示的文件类型,再查找要显示View方式,之后动态生成专用的View/Frame。唉,里面真是很复杂啊,想想都头晕,不管啦,反正都是那些天才们的事。呵呵,不过我也把文档视/图结构理解一种模型,也是一种方法,即把A—C模型变成A—B—C模型。
C++对象模型:
按我的理解,C++对象模型就是为了实现C++中面向对象程序设计的有关性质设计出来的有关类的数据成员、函数成员怎样在内存中安排(才能更高效地实现面向对象的性质)的一种类的模型。为了实现多态,对象模型便不得不多设计了一张virtual table表,而且这张表要考虑单继承多继承所带来的问题,还要考虑指向这张表的指针地址放在为对象分配得来的内存(以二进制形式顺序存放对象成员)的什么位置才理想,派生类特别是有多个基类的派生类怎么“继承”这张虚表,同时也要考虑怎么“继承”基类的其它成员(很多时候,派生类都要复制所有基类的所有成员,因此而显得浪费内存,确也无可奈何,所以,如果可以最好尽可能少派生类,并且一定要从最“基”的类派生)。也因为要实现多态,继承,封装等等性质,类的构造函数、析构函数,也变得复杂起来了,要考虑很多的问题,因为类与类之间不再独立了,他们之间一但有联系,事情也复杂起来了。类的函数成员,数据成员,在这种情况下,日子也不好过了,他们在内存中的安排也就要求有更高的技巧性了。基本上,你可以从观察C++对象模型中,知道为什么C++会有多态,继承这些特性,也知道C++语法为什么是这样子的,有什么限制等等。在学《深度探索C++对象模型》这本书的时候,我有这样的感觉:整本书各章间基本上是千丝万缕,各个章节要求你理解的道理都差不多,都要回归到内存分配这个话题上;所以,我建议,画两张有四五个类的详细类图,里面包含有类的各种关系,那么当我们看着这张图来学这本书的时候就会变得轻松多了,虽然里面也有图,但有点乱,因此我画了两张类图,以求精简完整。
COM与注册表:
COM接口是什么?按我现在的理解是,如我先前所说的模型中,把A—C模型变成A—B—C模型中的B中间层,A和C都是相对独立的可执行程序,它是一种协议一种规则,用于把A和C连系起来,为他们通信提供方便。COM接口定义了一组虚函数,这些函数的功能都将在A(或B)中得以实现;接口也是类结构并且还是A(或B)的父类,根据C++多态性,我们知道,通过父类就可以调用派生类的函数(只要指向的是派生对象的地址)了。现在问题是,怎么获取派生类的对象地址呢?我并不清楚。因为C++拥有多继承这一伟大性质,所以我们可以为A定义任义多个COM接口,利用嵌套类技术把COM接口的派生类插入A中以实现连接;比较复杂。
到目前为止,我仍然不能很好地理解注册表是什么东东,计算机怎么管理内存的;我看过不少那些号称是内幕技术能够知其所以然的书,但一讲到这两方面都是轻轻一笔带过,于是我只能靠猜了。表面上看来,注册表是一些符号串的集合,而这些符号听说是记录应用程序中的一些配置信息,应用程序需要记录一些什么样的信息呢?再让我想想,我们使用程序时,好像新启动的程序界面和上一次最后一次使用关闭时界面好像是一样的(大小,位置,颜色等),为什么会这样子呢?莫非是注册表里记录差这些信息,每次启动程序时都从注册表里取得这些信息?我怀疑是,还没得到证实。学COM的时候,书上好像也说过可执行程序文件的路径也记录在注册表里。我对内存管理很好奇,但对他的理解仅限于在学校课本里学到的,那远远不能满足我的求知欲,以后有空必定再学。
Application Framework:
刚开始学VC时,就常听到有人说MFC就是类库,对API进行了简单的封装;呵呵,果真如此吗?真是简单的封装吗?不!至少最重要最关键的几个类不是!MFC中几个最最重要的类,如CWinApp,CView, CFrameWnd,CDocument,CDocTemplate等;绝不仅仅是“简单的封装”!它们之互相合作,通过消息的流动而沟通,并且互相调用对方的函数,等等,互相调用函数?说起来好像挺不错的,但做起来呢,要它们几个类在互相调用而显得乱七八糟的情况仍能条理清淅,共同完成几乎我们所开发的所有的程序的基本框架,要考虑的问题何其的多!?Application Framework-----程序的基本框架?是的,我的理解就是这样,Application Framework核心思想就基于面象对向开发程序的思想,为我们做出一套完美的基本程序模型,我们所开发的程序都以它为基础。在面象对向语言C++之下,我们只需要从模型中原有的类中派生出自己的类,改写一些虚函数,定义一些新函数,就可以方便地完成我们想要的功能了,很多麻烦的事(如处理Document/View/Frame中先前我们讨论的麻烦事,程序初始化等)都由基本程序模型帮我们解决掉了。
应用程序与用户的交互:
固执的我一直想从C/C++语言出发去理解应用程序与用户之间的交互的问题;在我脑里常常想到的是程序里有一个main(),里面的程序代码是顺序地执行的;于是应用程序可以同时甚至交错地运行程序中不同地段的代码的问题就常常令我头痛。现在,在我看了不少源码后,我觉悟了,原来,我们所开发出的程序,除去启动程序的初始化操作中要顺序执行的代码外,还有很多死的代码段,它们并没有执行起来,和死的东西谈判顺序执行,我觉得自己很笨。不过,死是相对的,操作系统为我们激活这些代码段提供了一线光明:向程序发送消息。消息在操作系统的帮助下,送到应用程序中,应用程序就顺序执行一系列死的代码,包括调用执行消息所对应的函数,这时谈顺序执行才有意义。我们所发送的消息,都必须要有相应处理函数,这样消息才会有意义;各消息的处理函数之间,程序员应该尽可能让他们独立起来,办完自己的事就好了,函数执行完了就完了,不要与其它消息处理函数扯上太多的关系,这就是模块化思想。程序功能模块与功能模块之间大谈程序顺序执行是没意义的。而现在我还知道了顺序执行的本质并没有改变,只是因为有了个While循环用于捕捉消息才会变成现在这个看起来有点不可思议的样子。
我常常刻意地追求知其所以然,可事实往往与愿违。花太多的时间在这些理论上,而忽视了实践,就变成了肤浅;最可恨的是,以前辛苦追寻源代码而取得的收获,不仅在实践运用中没什么表现,而且还淡忘了;在发现API本身就是个大黑箱时,泪流满面,到头来,还是要查手册,还是要清楚了解API函数参数及功能才能够运用;使用API和使用那些控件一样,不知它在里面搞什么鬼。暗箱操作,我们无能回天,我们所能做的,也就只有尽可能详细地了解有关接口与参数问题了。
2002年7月10日