恶草丛生的阴暗角落---虚拟机制(上)

  摘要:C++是一个恶草丛生的地带,虚拟机制是很重要,但是很危险的一个C++特性,所以有必要对此作一下阐述,希望能对大家有所帮助。
       什么是虚函数
    简单地说,就是在成员函数前加关键字virtual,这样这个成员函数就变成了虚函数。
    虚函数的思想是从Simula借来得,在C++里面算得上最显著的特征。
    虚函数允许派生类取代基类所提供的实现。编译器确保当对象为派生类时,派生类的实现总是被调用,即使对象是使用基类指针访问而不是派生类的指针。
  
      我们为什么要用虚函数
    在虚函数开始被加进来的时候,人们对这个东东抱有强烈的抵触意识,有一种常见的说法是:虚函数不过是一个蹩脚的函数指针,完全是多余的。更有甚者,说什么良好的设计根本就不需要虚函数所提供的那些可扩展性和开放性。在这些观点别大肆批判之后,又产生了一种变形:虚函数值不过是一种低效的形式。为此大师们展开了一场保家卫国的战争,道路十分曲折,我不打算把这些观点重复一遍,如果你十分感兴趣,可以参考:The D&V of C++,TC++PL,What is OOP?在这里呢,我大概说一下他的重要性:
    从面向对象的角度看,如果没有虚函数,C++ 就不能算是面向对象的了。虽然重载很好,但不要忘了,它只是C概念中传递一个结构的指针给函数的句法装饰而已;虽然标准库包含了许多模板以实现同样非常好的“泛型编程”技术,但虚函数仍然是用C++进行面向对象编程的核心,通过在子类中override基类中的虚函数,就可以达到OO中的一个重要特性——多态。
    从商业角度看,如果没有虚函数,C++就不是面向对象的了,自然的我们也就没有什么理由要从C转到C++了。如果没有面向对象,我们就没有足够的理由去培训开发者、开发新工具,如果我们只有C++类的语法而没有面向对象的话,就不会减少维护成本,而实际上会增加培训成本。
    从语言的角度看,没有虚函数的C++不是面向对象,用类编程而没有动态绑定则只能算作“基于对象”,而不是“面向对象”。抛弃了虚函数,实际上就是抛弃了OO!结果就变成了早期的Ada语言。 
    
      虚函数和非虚函数调用方式有什么不同
    非虚成员函数是静态确定的,换句话说,该成员函数在编译时就会被静态地选择。
    然而,虚成员函数是动态确定的,换句话说,成员函数在运行时才被动态地选择,该选择基于对象的类型,而不是指向该对象的指针或引用的类型。这被称作“动态绑定”。大多数的编译器使用以下的一些的技术:如果对象有一个或多个虚函数,编译器将一个隐藏的指针放入对象,该指针称为vptr。这个vptr指向一个全局表,该表称为vtbl。在分发一个虚函数时,运行时系统跟随对象的vptr找到类的vtbl,然后跟随vtbl中适当的项找到方法的代码。
    虚函数对象的空间开销:每个对象一个额外的指针,加上每个方法一个额外的指针。
    虚函数对象的时间开销:和普通函数调用比较,虚函数调用需要两个额外的步骤。
    附:这里没有涉及诸如多继承,虚继承等内容,也没有涉及到我们已经说过的RTTI机制,更没有涉及诸如page fault,通过指向函数的指针调用函数等时空论的内容。
       虚函数和重载有什么不同
    虚函数看来于函数重载有些共通之处,但是函数重载在编译期间就可以确定下来我们要
使用的函数,是可预测的;而虚函数在运行时刻才能确定到具体的函数,是不可预测的
,对于虚函数这一特性有一个专用术语----晚绑定,运用虚函数这种方法叫做函数覆盖。
      虚函数遭遇内联
  呵呵,一个有趣的问题,但是回答往往不尽人意,特别是初学者更是如此。我发现初学者普遍认为序函数不可能是内联的,原因看起来似乎也很明显:
   (1)虚函数是在运行时机制而内联函数特性是一个编译时的机制;
   (2)声明一个内联的虚函数会使程序在执行的时间的产生多个函数拷贝,这将导致大量的空间的浪费。
  其实,在许多情况下,虚函数是都是静态确定的--特别是当派生类的虚方法调用其基类的方法时。你也许很奇怪为什么会这么做呢?答案很简单,就两个字:封装。一个很好的例子是,派生类的析构函数引起基类的析构函数的调用。除了最初的函数,其他的函数都是静态确定的。如果不确定基类析构函数为内联,就不能发挥这一优点。特别是在继承层次很深,并且许多对象被析构的时候,对虚函数进行内联毫无疑问会大大提高程序的运行效率。
 我们再举一个例子例子:
class Shape
{
public:
 inline virual void draw()=0;
};
inline void Shape::draw()
{ cout<<"Shape::draw()"<<endl; }
class Rectangle:public Shape
{
public:
 void draw()
         { Shape::draw(); cout<<"Rectangle::draw()"<<endl; }
};
Shape* p=new Rectangle;
p->draw();
这个draw是内联的吗?不,当然不是。这要通过虚函数机制在运行时刻确定。这一调用被转换为类似于下面的一些东西:
   ( *p->vptr[ 1] )( p );
  1代表draw在虚函数列表中的位置。因为这个draw的调用通过函数指针_vptr[1]来实现,编译器不能再编译时刻确定调用函数的地址,所以函数不可为内联。
  当然,内联虚函数draw的定义必须在某个地方出现以保证执行代码调用的恰当的运行。也就是,至少需要一个定义来在虚函数列表中放置它的地址。编译器如何确定在什么时候生成那个定义呢?一个方法是在虚函数列表生成的时候就生成定义。这意味着为每个类的实例生成一个虚函数列表。每一个内联函数的实例也同时产生。
  在一个可执行程序中为一个类要生成多少虚函数列表呢?恩,虽然标准对虚函数的行为做了一些规定;但是没有对实现做出约束。因为虚函数列表没有在标准中做出规定。所以明显也不会去规定如何控制虚函数列表或者生成多少实例。
  此外,C++标准现在要求内联函数表现得好象在一个程序中只有一个内联函数的定义,即使函数是在不同的文件中定义的。新的规定是使实现表现为只有一个实例产生。当标准的这个特性被广泛实现的时候涉及到代码膨胀的潜在问题也将会消失。
未完(待续...)
2.下,来源;http://www2.langzhou.com/lz_31357_ab.htm
作者:未知

 
作者: hustli



    虚函数的开销

  人们一提到虚函数,首先想到的是多态,紧接着想到的就是开销(至少我开始的时候就是这样子的),那么虚函数的开销来自哪里?开销究竟有多大?

  在理论上来讲,虚函数所带来的动态开销主要依赖于3个方面:编译器、操作系统和机器。但是在现实中,几乎所有的编译器都以同样的方式操作。调用一个虚函数的开销主要来自于2个方面,一个是如果虚函数不是内联的,就要增加一些额外的机器指令,不过一般来说也就增加3-5个机器指令(是从那里看的,既不清楚了,不过这个结论我倒是记得很清楚),从时间上来讲,与一个非虚拟函数相比,也就多花费10%-20%的开销,如果有几个参数,这个比例会更小。同时,函数调用的连接开销通常只是总的开销的一小部分,所以基本上来讲,序函数的开销可以忽略不计。当然,如果你在程序中大量运用虚拟函数,效率自然就会有很大的下降,积少成多么。

  虚析构函数的时机

  析构函数能不能是虚拟的,这个问题大家都比较清楚:能!那么什么时候把析构函数设计成虚拟的呢? 

  初学者往往会犯这样的错误:基类析构函数当然应该始终是虚拟的!

  看来这个规则比较的容易记住,当然她也有自己的理论基础(虽然分析的不怎么全面):一个类既然可以作为基类,那么他就打算多态的使用,因此就应该将析构函数设成虚拟的;另外由于目前大多数编译器都采用vtbl的手法实现虚拟函数,而vtbl是一个类共享的,所有的类的实例都共享这个vtbl,换句话说,如果一个类里面已经有了一个虚函数,那么把析构函数声明为一个虚函数不会对每个实例对象有什么空间开销。

  不过这样的分析还是不准确,例如下面的例子

  class base

    <

      public:

           ~base()

    >;

    class derive:public base

    <

     public:

          ~derive() < cout<<"derive!"<<endl; >

    >;

    derive d;

    看看他有没有调用~base?base虽然作为基类,但是它的析构函数并不需要声明为虚拟的。那么什么时候才必须声明成虚拟的呢?

  更加准确的规则如下:如果派生类有一个特殊的析构函数,并且我们也需要动态的删除基类的指针,那么这个基类的析构函数就应该是虚拟的。

  虚构造函数

  既然我们有了虚析构函数,那么有没有虚构造函数呢?

  很不幸的是,没有!原因在于虚拟调用是一种能够在给定信息不完全的情况下工作的机制。特别地,虚拟允许我们调用某个函数,对于这个函数,仅仅知道它的接口,而不知道具体的对象类型。但是要建立一个对象,你必须拥有完全的信息。特别地,你需要知道要建立的对象的具体类型。因此,对构造函数的调用不可能是虚拟的。

  不过,还能够给大家一个安慰的是,我们可以模拟虚构造函数。

  在tc++pl第15.6.2.节bjarne大师给出了一个例子。无独有偶,在《thinking in c++》的作者也给出了一种模拟虚构造函数的方法,基本是这样的。

  // 给出一个抽象类shape,里面有要提供的接口

  class shape

  <

  public:

    shape();

    virtual ~shape();

    virtual void draw();

    // ....

  >;

  class circle : public shape

  <

  public:

    circle(); ~circle();

    void draw();

    // ...

  >;

  class rectangle : public shape

  <

  public:

    rectangle(); ~rectangle();

    void draw();

    // ...

  >;

  // 再给一个shapewrap封装一下

  class shapewarp

  <

  protected:

    shape *object;

  public:

    shapewrap(const string &type)

    <

      if (type=="circle")

        object=new circle;

      else if (type=="rectangle")

        object=new rectangle;

      else

      <

        // ...

      >

    >

    ~shapewrap() < delete object; >

    void draw() < object->draw(); >

  >;

  为什么成员函数默认不是virtual的?

    因为很多类并不是被设计作为基类的。例如复数类。

    而且,一个包含虚拟函数的类的对象,要占用更多的空间以实现虚拟函数调用机制——往往是每个对象占用一个字(word)。这个额外的字是非常可观的,而且在涉及和其它语言的数据的兼容性时,可能导致麻烦(例如c或fortran语言)。

    要了解更多的设计原理,可以参考the d&e of c++。

   成员函数的模板可不可以是virtual的?

    ansi/iso 的标准说 (14.5.2 p 3): "a member function template shall not be virtual."(一个成员函数的模板不可以是virtual.)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值