现在的位置: 首页 > 自动控制 > 工业·编程 > 正文

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

2012-08-17 11:29 工业·编程 ⁄ 共 2556字 ⁄ 字号 暂无评论

    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++标准现在要求内联函数表现得好象在一个程序中只有一个内联函数的定义,即使函数是在不同的文件中定义的。新的规定是使实现表现为只有一个实例产生。当标准的这个特性被广泛实现的时候涉及到代码膨胀的潜在问题也将会消失。

未完(待续...)

给我留言

留言无头像?