C 多态技术的实现和反思

2008-02-23 05:24:13来源:互联网 阅读 ()

新老客户大回馈,云服务器低至5折

面向对象技术最早出现于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算法获得最优的散列函数。


  事实上,我们还能够这样理解这种方案的优势,把表中每一项的“方法名”项视为“方法地址”项的描述信息,因此能够认为这种方案中的方法查找表携带自描述信息(或称为元数据)。基于这种携带自描述信息的数据结构,能够实现丰富多彩的扩展功能,比如在运行时
插入新的方法,或用户层次上的方法调用截获等。因此,我们能够说这一方案的适用面广,强大灵活,但在执行效率上并非最优。

  另一种虚方法查找方案则是C 研发者十分熟悉的,基于绝对位置的定位技术。其查找表结构很简单,仅仅是个存放了方法地址的指针数组。表中的每一项不具备自描述性,只有编译器在编译时知道他们究竟分别对应着哪一个方法,并且将对于方法的调用代码编译成一个紧凑的指针 偏移的调用的硬编码。这种查找表的最大特点就是高效率,基于这种查找表进行方法调用仅仅需要多做一次数组内的随机访问操作。在任何我们所能想到的“增加一个间接层”的方案中,这种方案在效率上是最高的。但是使用这种方案有一个限定,就是需要任何同族多态对象具备完全相同的查找表。也就是说,您必须确保任何实现了某个接口的对象的虚方法查找表的第k 项都具备相同的语义。假设一个基类有100个可供改写的虚方法,那么他的虚方法查找表共有100项(实际上就是100个指向方法入口地址的指针)。而其任何派生类对象都必须有结构上完全相同的、长度至少为100项的虚方法查找表。现在假设我们研发的一个派生类中只改写了基类的5个方法,那么这个派生类对象所共享的虚方法表仍然长达100项,只但是其中95项和其基类对象虚方法查找表中相应的项一模相同,只有5项具备实际意义——正是这5项的存在才使派生类的存在有了意义。

  在这种情况下,该方法表的实际有效利用率只有可怜的5%。总的来说,这一方案执行效率最优,但是并不适用于任何的场合。

标签:

版权申明:本站文章部分自网络,如有侵权,请联系:west999com@outlook.com
特别注意:本站所有转载文章言论不代表本站观点,本站所提供的摄影照片,插画,设计作品,如需使用,请与原作者联系,版权归原作者所有

上一篇: C 的未来之路:C 0x概览

下一篇: C 数据类型的属性和限制