当前位置:天才代写 > tutorial > C语言/C++ 教程 > C++多态技能的实现和反思

C++多态技能的实现和反思

2017-11-06 08:00 星期一 所属: C语言/C++ 教程 浏览:417

副标题#e#

面向工具技能最早呈现于1960年月的Simula 67系统,而且在1970年月保罗阿托尝试室开拓的Smalltalk系统中成长成熟。然而对付大部门措施员来说,C++是第一个可用的面向工具措施设计语言。因此,我们关于面向工具的许多观念和思想直接来自于C++。可是,C++在实现面向工具中要害的多态性时,选择了与Smalltalk完全差异的方案。其功效是,尽量在外貌上两者都实现了相似的多态性,可是在实践中却有着庞大的区别。详细的说,C++的多态性实现越发高效,可是并不合用于所有场所。许多履历不敷的C++开拓者不大白这个原理,在不符合的场所强行利用C++的多态性机制,落入削足适履的陷阱而不能自拔。本文将具体探讨C++多态性技能的范围性及办理的步伐。

两种差异虚要领挪用实现技能

C++的多态性是C++实现面向工具技能的基本。详细的说,通过一个指向基类的指针挪用虚成员函数的时候,运行时系统将可以或许按照指针所指向的实际工具挪用得当的成员函数实现。如下所示:

class Base {
   public:
   virtual void vmf() { ... }
   };
   class Derived : public Base {
   public:
   virtual void vmf() { ... }
   };
   Base* p = new Base();
   p->vmf(); // 这里挪用Base::vmf
   p = new Derived();
   p->vmf(); // 这里挪用
// Derived::vmf
   ...

请留意代码中突出注释的两行,固然其外貌语法完全沟通,可是却别离挪用了差异的函数实现。所谓的“多态”即就此而言。这些常识是每一个C++开拓者都熟知的。

此刻我们假设本身是语言的实现者,我们该当如何来实现这种多态性?稍加思考,我们不难获得一个根基的思路。多态性的实现要求我们增加一个间接层,在这个间接层中拦截对付要领的挪用,然后按照指针所指向的实际工具挪用相应的要领实现。在这个进程中我们工钱增加的这个间接层很是重要,它需要完成以下几项事情:

1. 获知要领挪用的全部信息,包罗被挪用的是哪个要领,传入的实际参数有哪些。

2. 获知挪用产生时指针(引用)所指向的实际工具。

3. 按照第1、2步得到的信息,找到符合的要领实现代码,执行挪用。

这里的要害在于如安在第3 步中找到符合的要领实现代码。由于多态性是就工具而言的,因此我们在设计时要把符合的要领实现代码与工具绑定到一起。也就是说,必需在工具级别实现一个查找表布局,按照1、2步得到的工具和要领信息,在这个查找表中找到实际的要领代码地点,并加以挪用。此刻问题酿成了,我们该当按照什么信息举办要领查找。对付这个问题有两个差异的办理思路,一个是按照名称举办查找,另一个是按照位置举办查找。粗看上去这两种思路好像没什么大的不同,可是在实践中,这两种差异的实现思路导致了庞大的不同。下面我们具体地加以考查。

在Smalltalk、Python、Ruby等动态面向工具语言中,实际要领的查找是按照要领名称举办的,其查找表布局如下:

由于这种查找表按照要领的名称举办要领查找,因此在查找进程中涉及字符串较量,效率较差。可是这种查找表有一个突出的利益,就是有效空间操作率高。为了说明这一点,我们假设一个基类Base中有100个要领可供派生类改写(因此所有Base工具所共享的要领查找表有100项),而它的一个派生类Derived仅仅只规划改写个中5个要领,那么Derived类工具的要领查找表只需要5项。当一个要领挪用产生的时候,runtime按照被挪用的要领名称在这个长度为5 的要领查找表中举办字符串查找,假如发明该要领在查找表中,则执行挪用,不然将挪用转寄(forward)给Base类执行。这是虚要领挪用的尺度行为。当派生类实际改写的要领数量很少的时候,可以将查找表布置成线性表,查找时顺序较量,这种环境下有效空间操作率到达100%。假如派生类实际改写的要领数量较多,那么可以回收散列表,假如回收公道的散列函数,同样可以在空间操作率很高(一般靠得住近75%).. 的环境下实现要领的快速查找。该当留意到,由于编译器可以很容易地得到所有被改写要领的名称,因此可以执行尺度的gperf算法得到最优的散列函数。


#p#副标题#e#

C++多态技术的实现和反思

事实上,我们还可以这样领略这种方案的优势,把表中每一项的“要领名”项视为“要领地点”项的描写信息,因此可以认为这种方案中的要领查找表携带自描写信息(可能称为元数据)。基于这种携带自描写信息的数据布局,可以实现富厚多彩的扩展成果,好比在运行时

插入新的要领,可能用户条理上的要领挪用截获等。因此,我们可以说这一方案的合用面广,强大机动,但在执行效率上并非最优。

#p#分页标题#e#

另一种虚要领查找方案则是C++ 开拓者十分熟悉的,基于绝对位置的定位技能。其查找表布局很是简朴,仅仅是一个存放了要领地点的指针数组。表中的每一项不具有自描写性,只有编译器在编译时知道它们毕竟别离对应着哪一个要领,而且将对付要领的挪用代码编译成一个紧凑的指针+偏移的挪用的硬编码。这种查找表的最大特点就是高效率,基于这种查找表举办要领挪用仅仅需要多做一次数组内的随时机见操纵。在所有我们所能想到的“增加一个间接层”的方案中,这种方案在效率上是最高的。可是利用这种方案有一个限定,就是要求所有同族多态工具具有完全一样的查找表。也就是说,你必需确保所有实现了某个接口的工具的虚要领查找表的第k 项都具有沟通的语义。假设一个基类有100个可供改写的虚要领,那么它的虚要领查找表共有100项(实际上就是100个指向要领进口地点的指针)。而其所有派生类工具都必需有布局上完全沟通的、长度至少为100项的虚要领查找表。此刻假设我们开拓的一个派生类中只改写了基类的5个要领,那么这个派生类工具所共享的虚要领表仍然长达100项,只不外个中95项与其基类工具虚要领查找表中相应的项一模一样,只有5项具有实际意义——正是这5项的存在才使派生类的存在有了意义。

在这种环境下,该要领表的实际有效操作率只有可怜的5%。总的来说,这一方案执行效率最优,可是并不合用于所有的场所。

虽然,看上去上述两种虚要领挪用实现技能结果完全一样,一切都被掩盖在编译器之下,与一般开拓者毫无干系。可是,事实真的如此吗?我们在下面会看到,C++ 的这种查找表布局组成了C++应用开拓中最险恶的技能陷阱之一。

两种差异的多态性应用场景

进修过数值阐明的读者应该熟知,在矩阵运算的电算求解规模,低阶浓密矩阵的求解与高阶稀疏矩阵的求解是性质完全差异的两个问题,其存储方案和求解算法截然差异。很是有趣的是,在多态性的实际应用中,也有着与矩阵问题雷同的两种性质上截然差异的场景。

第一种场景中,我们所结构的工具较量简朴,同一族系中兄弟类总数不多,而互相之间的差别较大,因此工具中的虚要领数量少,而改写率高。我们凡是在教科书上所打仗的面向工具例子,以及在一般应用规模中打仗的工具都属此类。

譬喻一个Modem类,纵然其具有较多的特性,虚要领总数也很难高出20个,而差异的Modem类实现,大概会改写个中大部门甚至全部虚要领。另一个例子是COM接口。由于COM组件思想基于接口,而一个粒度精采的接口一定是“瘦小干练”的。好比IMalloc接口只有6个要领(不包罗从IUnknown担任来的3 个要领),IPersistFile共5个要领,凡是用户本身写的COM接口中的要领数量也不高出20。而在实现COM接口是,险些老是需要改写全部要领。这与低阶浓密矩阵很是相似,因此值得用最简朴直接的查找表布局来实现——速度快,并且简朴直接。由于虚要领改写率高,查找表中的有效操作率较高。这种场景是C++多态性实现技能大大的用武之地,可以说C++特色的虚要领挪用机制就是用来应对这种应用的。

#p#副标题#e#

而第二种应用场景截然差异,在这种场景中,工具较量巨大,特性浓密,行为变革多端,同一族系中兄弟工具数量复杂,而互相之间大同小异。此种工具中的虚要领数量多,而改写率低。GUI系统和视频游戏是这种应用场景的典范代表。由于我们成天与Windows 系统打交道,所以用Windows GUI系统来说明这种场景是最符合不外的了。我们知道,在Windows图形界面上的险些所有实体从观念上讲都是Window工具,因此组成了一个工具族系。这个族系有三个突出的特点。一是行为多,特征多变(可能说虚要领数量多)。Microsoft Windows系统直接界说了数百个窗口动静,并答允用户利用WM_USER+n和WM_APP+n的方法界说新的动静,用面向工具的话来说,就相当于给Windows系统中的所有Window工具界说了数百个可供改写的虚要领,而且还答允用户自由扩展新的虚要领。

第二个特点是改写率低,同族工具之间大同小异。凡是我们对付绝大大都的窗口动静都是用DefWindowProc来统一处理惩罚,可能用SendMessage函数将动静转发(委托)给系统提供的尺度窗口工具处理惩罚,这也就是相当于把这些动静交给基类窗口工具来处理惩罚,而只拦截(改写)个中几个至几十个动静(要领)。相对付窗口工具族复杂的虚要领数量来说,改写率凡是不高出20%。第三个特点是同族兄弟类数量复杂。从尺度窗口到异型窗口,从对话框到按钮,从东西条到文本框,所有的一切都是Window,甚至于两个按钮看上去完全一样,仅仅是caption差异,按下时执行的操纵差异,就需要用差异的类来结构。因此在一个普通局限的应用措施GUI界面系统中,结构上百个大同小异的窗口类是并不奇怪的。任何一个对Win32 API有必然领略的开拓者,对此都不难体会。

#p#分页标题#e#

从第1节对付C++虚要领挪用机制的先容可以很容易地知道,C++那种基于绝对位置的、不带任何自描写信息的查找表布局,并不合用于上述的第二种场景。假如强行利用C++原生的工具模子来实现雷同Windows的GUI系统,那么功效是这样的:基类(不妨设为KWindow类)要界说1000个虚要领(个中应该留出几多位置供用户扩展之需呢?),从而拥有一个长达1000的查找表,而所有的直接和间接派生类工具,为了保持与KWindow 在要领查找表布局上的兼容,都要至少海涵一个长达1000的查找表。

我们举一个极度的例子来浏览一下这种办理方案的谬妄性,假设有一个类KPushButton从KWindow中派生,并通过改写20个虚要领实现了一个尺度的按钮控件,那么它的虚要领查找表中有几多项?对不起,不是20 项,而是至少1000项(假如它没有插手新的要领的话),个中绝大大都仅仅是KWindow虚要领表的原封不动的克隆,只有20项属于它本身,只有这20项真正有意义,要领表中980项被挥霍掉了。它们独一的意义在于占据一些位置,使得“指针加偏移”的计较可以或许继承精确地寻址。你觉得工作已经很糟糕了?不,事实上还可以更糟糕!

假设你需要一个尺度按钮,它的外观、颜色、文字和其他行为都与KPushButton完全一样,仅仅是相应CLICK事件的操纵差异,你需要怎么办?显然是从KPushButton中派生本身的KMyPush-ButtonOK类,然后改写个中的1 个要领(大概是叫做OnClick的)。那么在这个新的类中,虚要领表是多长呢?是1项吗?不是。是20项吗?也不是。实际上,是1000项!个中只有1项(OnClick)浮现了它存在的意义,其他999项(在32位呆板上占据3996个字节)险些完全被挥霍掉了!一其中等局限的应用措施中布置几十个界面,数百个自定制控件,则仅在虚要领表上挥霍的存储空间即到达数百KB甚至1MB以上。也许这个数字在本日用GB 大筐装主存的时代实在是小儿科,可是其背后所浮现的思路之丑恶却是任何一个有点本心的开拓者(尤其是C++开拓者)所不能容忍的。

也正是因为这个原因,从OWL 到VCL,.. 从MFC到Qt,以至于近几年呈现的GUI和游戏开拓框架,所有涉及大量事件行为的C++ GUI Framework没有一家利用尺度的C++多态技能来结构窗口类条理,而是各自为战,发现出八门五花的技能来绕过这个暗礁。个中较量经典的办理方案有三,别离以VCL 的动态要领、MFC的全局事件查找表和Qt 的Signal/Slot为代表。而其背后的思想是一致的,用Grady Booch的一句话来总结,就是:“当你发明系统中需要大量相似的小型类的时候,该当用大量相似的小型工具办理之。”2 也就是说,将一些原来会导致需要派生新类来办理的问题,用实例化新的工具来办理。这种思路险些一定导致雷同C#中delegate那样的机制成为必须品。惋惜的是,尺度C++ 不支持delegate。固然C++社群里有许多人做了各类尽力,应用了诸如template、functor等高级能力,可是在结果上间隔真正的delegate尚有差距。因此,为了保持办理方案的简朴,Borland C++Builder扩展了__closure要害字,MFC发现出一大堆怪模怪样的宏,Qt搞了一个moc前处理惩罚器,八仙过海,各显神通。

让我们小结一下,面向工具多态性有两种差异的应用场景,而C++的尺度多态技能只适合个中一种,对付另一种并不适合,必需以其他机制实现。

办理思路和发起

或者有读者读到这里,会对C++发生很大的猜疑。需要说明的是,C++选择的多态性实现技能是完全切合C++哲学的。并且,C++答允你以各类大概的步伐来办理这个问题。时至今天,依靠各类成熟的GUI框架,大大都环境下我们可以自动绕过暗礁。

问题的严重性在于,由于C++教诲上的问题,许多开拓者对付C++原生多态技能在上述第二种应用场所中的范围性认识不敷,因此当他们面对雷同的问题时,会不自觉地踏入陷阱中。在此我愿提醒C++开拓者,当你面临的系统中含有尺度的事件处理惩罚特征,并且事件数量较大时,请慎重思量你的类条理布局设计。可以思量仿照MFC可能Qt的办理要领,但在我看来,一个越发直接并且简朴的要领是,模仿本文第1节中描写的、基于字符串较量的要领查找表,用一个单一的动静分发工具来向各个工具分动员静。由于这个动静分发工具会常常需要调解变革,将它单独放在一个DLL 甚至COM组件中,在运行时加载到历程内。这种方案不是最精良的,可是在大大都环境下有效,而且实现起来较量简朴。限于篇幅,这里不具体描写。

#p#分页标题#e#

事实上,我本人认为,C++语言该当从编译器上办理这个问题。根基思路为,当基类虚要领数量大而派生类改写的要领数量小的时候(这个信息可以从编译进程中获得),改变派生类工具的虚要领查找机制,改按位置查找为按被挪用函数实际信息查找。这样一来,派生类中的虚要领表可不必与基类保持布局上的一致,从而制止了空间上的挥霍。这种思路跟Delphi/Object Pascal语言中dynamic要害字有相似之处。本文不再赘述。

 

    关键字:

天才代写-代写联系方式