软件接口的消息方法和虚函数方法的比较

2012-04-29 00:44:03徐明毅
教育教学论坛 2012年1期
关键词:接口

摘要:消息方法和虚函数方法广泛用作软件接口,本文对它们的各自特点及在MFC和COM中的使用特点进行了评述。消息方法一般适用性广,但速度慢;虚函数方法速度快,但适应性差。综合两者的特点,提出了包含消息处理的虚函数方法和构造内部函数表的消息处理方法,该两种方法在VC6.0中测试通过,便于在软件设计时灵活选择。

关键词:接口;消息处理;虚函数;MFC;COM

中图分类号:TP311.5 文献标志码:A 文章编号:1674-9324(2012)01-0073-03

在可扩展的软件体系中,常常在模块之间设定一定的接口,通过接口来适应未来的系统扩充需要。狭义的接口是指只包含虚函数的类,这种类通常没有数据成员;广义的接口是指软件模块之间的联结纽带,该纽带保证各模块之间可互不干扰地独立升级,便于大型软件系统的多人并行开发。接口的定义方式有多种,如全局函数方式,类的成员函数方式,类的虚函数方式、消息方式等,其中灵活性较强、便于扩展且常用的是消息方式和虚函数方式。在实际应用系统中,Windows窗口系统就是采用的消息模式来扩展视窗的功能,窗口程序开发框架MFC也是基于消息方法来架构的1]。而从动态链接和嵌入(OLE)发展起来的ActiveX控件技术则是基于COM接口2],该接口建立在虚函数方法上。本文对这两种接口进行了比较,提供了简单的教学测试用例,以便在实际开发中采用适合的接口模式。

一、通过消息函数提供接口

消息方法的接口是使用一个消息函数来处理所有可能的软件功能扩展,每一个功能对应一个消息ID号,然后根据消息类别来解释参数的含义,进行相应的处理。以下用简单的例子来进行说明,比如,先设定几种消息的编号,对应不同的功能:#define MODE_1 1;#define MODE_2 2,然后,编写消息函数如下:void msgproc(int msg,int para){switch(msg){case MODE_1:printf("This is mode 1!");break;case MODE_2:printf("This is mode 2!");break;}}消息函数传入一个消息定义值,另外预留一个参数,通过对消息值的一一判断,采取相应的动作。在主程序中测试时,就可以用同一个消息函数调用不同的功能。msgproc(MODE_1,0);msgproc(MODE_2,0);以上例子很简单,但说明了消息函数的工作原理,即根据消息值进行分支处理。在Windows系统中,窗口的标准消息函数原型为:WindowProc(HWND,UINT,WPARAM,LPARAM);第一个参数为窗口句柄,第二个参数为消息编号,第三和第四个参数为预留的消息处理可能要用到的其它参数,因此,Windows窗口的消息函数共4个参数,每个参数的大小都是4个字节。不管是什么窗口界面程序,核心的处理函数就是该消息函数。在窗口增加新的命令时,消息类型都为WM_COMMAND,而参数WPARAM为该命令的编号ID,LPARAM为可能用到的其它参数。通过这种方法,可不断增加新的命令,只要新命令的ID号与原有的不重复,就可以在保持兼容的情况下不断扩展。从以上可以看出,软件接口的消息模式的优点是:①接口简单,便于支持各种程序语言,便于跨语言扩展;②可不断扩充新的消息类型,扩展性强,兼容性好。但消息方法在有以上优点的同时,也具有以下缺点:①消息函数要处理所有消息,在消息种类太多时,会使消息处理函数十分庞大。②消息函数的类型是固定的,能传递的参数有限,必须通过传递结构指针或全局数据的方式来传递其余参数,过程繁琐,安全性差;③消息处理函数一般需要对消息类型逐一进行判断,效率不高,在软件功能模块越来越多时,会产生反应迟钝的现象。为了提高处理速度,可以采用另一种接口方法,即虚函数方法。

二、通过C++的虚函数方法提供接口

C++语言具有面向对象的功能,即设定一个虚拟基类(父类),然后子类从父类派生,但可以继承同样的虚拟接口,然后外部就可用父类的接口调用所有的子类,也就是所谓的面向对象方法。这实际上是在软件模块之间设定了一个规定的接口,达到先分离再组合,灵活扩展软件功能的目的。例如,设定一个父类为:class base {public:virtual void mode1()=0;virtual void mode2()=0;};通过设定一些接口为虚函数,表示继承的子类将具有同样的接口形式,但具体的表现却可以不同。如派生一个子类为:class child:public base {public:void mode1(){printf("This is mode 1!"); }void mode2(){printf("This is mode 2!"); }};外部调用时,就可以产生一个具体的子类,然后用父类的接口进行调用:child r;r.mode1();r.mode2();如果有不同的子类,可以有不同的表现,但调用形式完全一样,这就达到了灵活扩充系统功能的作用。使用时,先将子类转换为父类,然后统一用父类的虚拟接口来调用,如:base* pBase = new child;pBase ->model1();pBase ->model2();delete pBase;需要注意的是,在释放该对象时,如果统一用父类指针释放,则需要将析构函数也设置为虚拟函数,才能正确地调用子类的析构函数。如果不使用虚拟析构函数,则需要先转换为子类的指针,才能安全析构该子类。在虚函数的内部实现中,一般是在内存中构造一个函数表,虚拟函数的调用是函数表的位置加上一个偏移值来确定的3]。这增加了一层间接性,同时也增加了一层灵活性。与函数调用相比,增加了一个根据偏移值取函数地址的过程,该操作显然比消息方法的逐一判断要快得多。因此,虚函数方法的优点是:(1)根据虚函数的位置取址后调用处理函数,只需进行一个索引操作,速度快;(2)支持继承体系,同一接口可对不同的派生对象操作,而父类的虚拟函数接口保持不变。由于虚表的构造是编译器内部处理的,因此又具有以下缺点:(1)扩充处理函数时不方便,由于需要对内部的虚函数表进行扩充,一般需要重新编译,不能做到二进制上的即插即用;(2)由于各种语言的虚函数表的构造方法并不统一,故难以进行跨语言扩展。为了将虚函数表的布局方法统一起来,微软公司提出了二进制上兼容的COM接口标准,ActiveX控件就采用该标准。

三、提高虚函数的可扩充性

有时候,父类的虚函数种类已经固定了,添加新的虚函数很困难,容易引起兼容性问题。如何在这种情况下保持尽量多的扩充性呢?这可以借鉴消息函数的方法。将一个虚函数设置成消息函数的形式,传入消息类型和必要的处理参数,就可以适应未来的扩充需要。如在父类中设置一个虚拟的消息函数形式:virtual void mode3(int msg,int para)=0;在子类中实现该函数为:void mode3(int msg,int para){switch(msg){case MODE_1:printf("This is extent mode 1!");break;case MODE_2:printf("This is extent mode 2!");break;}}可以看到,该函数与一般的消息函数是类似的。外部调用时,可以采用:r.mode3(MODE_1,0);r.mode3(MODE_2,0);

因此,为适应软件扩充的需要,对于已经较为固定的处理类型,可在父类中设定虚函数类型,子类中实现即可;而对于可能扩充的功能,可用虚拟消息函数的形式保持接口,便于扩展。

四、提高消息函数的处理速度

虚函数的接口方法天生具有速度优势,但使用虚函数需要确定函数类型,在系统初始设计时可能无法给定,如果采用虚拟消息函数的形式,则处理速度下降,体现不出速度优势。那么,在消息方法中,能否提高消息函数的处理速度呢?实际上,消息模式下也可构造函数地址表,达到和虚函数方法相当的调用效率。首先将每种功能调用设置为函数,如:void func1(){printf("This is mode 1!");}void func2(){printf("This is mode 2!");}然后定义函数类型:typedef void (*PFUN)();在消息处理函数内构造函数表,让它和消息值对应起来,根据消息值直接查找到函数指针,然后调用即可。void msgproc1(int msg,int para){static PFUN table]={0,func1,func2};tablemsg]();}而外部调用和通常的消息函数完全一样:msgproc1(MODE_1,0);msgproc1(MODE_2,0);

在消息类型很多时,该方法明显快于分支判断方法,可以达到和虚函数方法同等级别的处理速度。但缺点是对消息值的取值有所限制,因为要将消息值作为函数表的索引值来使用。

五、MFC的消息映射方法分析

MFC是微软开发的用于Windows界面程序设计的基本类库[1],MFC中的消息映射机制没有采用虚函数,是否是因为虚函数的空间代价呢?虚函数的实现需要占用内存,一个类的虚表的大小就是虚函数的个数乘以一个指针的大小。假设Windows的通用消息有200个,用虚函数方法实现的话,视窗类的虚表就有200*4个字节= 800字节,所有的视窗类的派生类都要承受800字节的代价。假设有100个类派生自视窗类,那么代价就是800*100字节也就是80K字节。在MFC2.0版本发布的时候是1992年,当时个人台式机的内存才几兆,这是值得考虑的因素了。由于虚表是和类绑定在一起的,而不是和类对象(也叫类的实例)绑定在一起的,类的实例仅增加一个指向该类虚表的指针而已。也就是说,如果你有100个视窗派生类,哪怕生成了10000个派生类的实例,虚表占用的内存也是80K,但派生类的每个实例要保留指向虚表的指针,即4 字节,因此增加了40 K字节的内存使用。从这点来看,似乎MFC没有采用虚函数,内存的确是一个考虑因素。但实际上,只要类具有虚函数,就必然有指向虚表指针的内存消耗,而MFC中的常用消息处理类都是具有虚函数的,所以该负担并非由消息处理机制引起。因此可以说,MFC的消息机制没有采用虚函数,并非因为虚函数的空间占用问题,着眼点应该是侧重于容易增加新的消息类型。如果使用虚函数机制来实现消息处理,对于每个可能的消息都必须在基类中定义一个虚函数,而其首要的困难是无法猜测未来会出现什么消息,也无法确定需要定义什么样的函数原型。而使用消息映射,解决这个问题则相对容易,因为这将由未来的程序设计者决定他们的消息该如何处理,所以消息函数方法的灵活性强于虚函数方法。当然,在消息类型已经固定、内存充足的情况下,采用虚函数方法(或COM接口),则又是值得考虑的方案了。

六、COM接口的虚函数方法分析

COM技术作为Windows平台上的组件对象模型2],为组件化程序设计提供了基础平台。它是在C++虚函数接口基础上提出的二进制标准,只要符合COM规范,不仅可以用C++语言打造COM接口,也可用其它语言打造COM接口,这为软件组件之间的交互提供了便捷高效的通道。以虚函数作为接口在功能扩充时有一定困难,一般认为:“接口一旦发布,不能修改”。其本质问题在于C++以虚表加上偏移值的方式实现虚函数调用,而偏移值又是根据虚函数声明的位置隐式确定的,这就造成了脆弱性。如果随意增加新的虚函数,造成虚表的排列发生变化,则现有的二进制可执行文件就可能调用到错误的函数。要扩充新的接口而又不影响原有的二进制文件,一种做法是:把新的虚函数放到接口声明的末尾。这么做不够优雅,因为新的虚函数与原有类似功能的函数不在一起;同时也很危险,因为该类如果被继承,那么新增的虚函数会改变派生类中的虚表偏移值变化,会影响派生类的接口,造成在二进制上不能兼容。COM 采用的较为安全的办法是通过链式继承来扩展现有接口,通过派生得到新类,得到新类后,更改版本号,就可使用新的接口了。该法和前面方法实质一样,但没有安全问题。这种带版本的接口扩充方法解决了二进制兼容性的问题,客户端源代码也不受影响。带版本的接口扩充解决了兼容性问题,但也造成管理上的困难。因为每次改动都引入了新的接口类,管理起来很麻烦。如果我们能直接原地扩充接口类,就不会同时管理如此多的接口类。在Windows和Linux系统中,核心功能接口一直在扩充,它保持兼容性的办法很原始,就是给每个系统调用赋予一个固定的数字代号,等于把虚函数表的排列固定下来,因此只要不与已有功能冲突,可以随时添加新的功能。这与消息函数的快速处理方法是一样的,也就是自己构造虚表,实现上更底层一些,扩充接口时只需声明新的功能号,适合于扩充范围较有限的情况。纯粹从接口使用方面来说,通过名称来调用接口更灵活一些,这就像使用Internet域名比使用 IP地址更普遍一样。虚函数是通过虚表加上偏移值进行调用,而非虚函数是通过名称进行绑定,通过内部决议名把可执行文件链接到一起,因此扩充时不会影响已有函数。因此非虚函数比虚函数更健壮,但困难是必须有一个编译和链接的过程,而且内部名称在不同语言中可能不一致,所以难以做到在二进制层面上即插即用。如果要采用跨语言的二进制软件模块,则需要暴露C语言的接口,然后用动态链接库的方式进行调用即可,这也是Windows系统中最广泛采用的二进制软件接口方法,即动态链接库方式。

通过以上比较可以看出,消息模式的接口方法适用性更广,而虚函数方法天生具有速度优势。因此,对于处理函数较为有限,固定且希望效率较高,可优先选用虚函数方法。但如果希望不断扩展功能,且采用不同的开发语言,则用消息模式更有利一些。两种方法可互相借鉴,在消息模式中可自定义函数表,使执行速度达到与虚函数方法相当,而虚函数方法也可定义一个特殊的消息处理虚函数,使之在接口固定的情况下,功能可不断扩充,或者遵循COM接口规范,通过新的接口类来扩展功能。因此在实际软件开发中,一般来说,如果开发语言单一,功能较为固定,速度要求高,则优先选取虚函数方法;如果跨语言开发,功能需不断扩充,而对速度要求不苛刻,则优先选取消息函数方法。两者的优势还可通过混合方式进行互补,以满足实际需要。

参考文献:

[1]侯捷.深入浅出MFC(第二版)[M].武汉:华中科技大学出版社,2001.

[2]潘爱民.COM原理与应用[M].北京:清华大学出版社,1999.

[3]Stanley B.Lippman.深度探索C++对象模型[M].侯捷,译.武汉:华中科技大学出版社,2001.

作者简介:徐明毅(1973-),男,重庆市人,副教授,主要从事水工结构数值模拟研究和结构分析软件开发。

猜你喜欢
接口
现场采购代表与总部及现场各部门的接口关系
某电站工程设计管理与施工、质量控制接口关系研究
脱硝数据传输系统远程无线监控技术的研发与应用
中文信息(2016年10期)2016-12-12 12:56:55
西门子SPPA—T3000在委内瑞拉燃机电厂中的应用与接口
中国市场(2016年32期)2016-12-06 11:16:14
高性能计算机管理软件基本原理研究
基于海洋石油XGIS平台组件式开发接口的研究与应用
居家环境监测系统研究
基于HIS的体检软件设计与应用
中俄网络语言编码接口问题的研究
科技视界(2016年3期)2016-02-26 10:14:32
企业整合为行业升级预留“接口”