为什么要用虚函数
(2015-05-09 00:05:08)
标签:
佛学 |
分类: 汇编/C/CC |
使用的函数,是可预测的;而虚函数在运行时刻才能确定到具体的函数,是不可预测的
,对于虚函数这一特性有一个专用术语----晚绑定,运用虚函数这种方法叫做函数覆盖。
呵呵,一个有趣的问题,但是回答往往不尽人意,特别是初学者更是如此。我发现初学者普遍认为序函数不可能是内联的,原因看起来似乎也很明显:
其实,在许多情况下,虚函数是都是静态确定的--特别是当派生类的虚方法调用其基类的方法时。你也许很奇怪为什么会这么做呢?答案很简单,就两个字:封装。一个很好的例子是,派生类的析构函数引起基类的析构函数的调用。除了最初的函数,其他的函数都是静态确定的。如果不确定基类析构函数为内联,就不能发挥这一优点。特别是在继承层次很深,并且许多对象被析构的时候,对虚函数进行内联毫无疑问会大大提高程序的运行效率。
class Shape
{
public:
};
inline void Shape::draw()
{ cout<<"Shape::draw()"<<endl; }
class Rectangle:public Shape
{
public:
};
Shape* p=new Rectangle;
p->draw();
这个draw是内联的吗?不,当然不是。这要通过虚函数机制在运行时刻确定。这一调用被转换为类似于下面的一些东西:
1代表draw在虚函数列表中的位置。因为这个draw的调用通过函数指针_vptr[1]来实现,编译器不能再编译时刻确定调用函数的地址,所以函数不可为内联。
当然,内联虚函数draw的定义必须在某个地方出现以保证执行代码调用的恰当的运行。也就是,至少需要一个定义来在虚函数列表中放置它的地址。编译器如何确定在什么时候生成那个定义呢?一个方法是在虚函数列表生成的时候就生成定义。这意味着为每个类的实例生成一个虚函数列表。每一个内联函数的实例也同时产生。
在一个可执行程序中为一个类要生成多少虚函数列表呢?恩,虽然标准对虚函数的行为做了一些规定;但是没有对实现做出约束。因为虚函数列表没有在标准中做出规定。所以明显也不会去规定如何控制虚函数列表或者生成多少实例。
此外,C++标准现在要求内联函数表现得好象在一个程序中只有一个内联函数的定义,即使函数是在不同的文件中定义的。新的规定是使实现表现为只有一个实例产生。当标准的这个特性被广泛实现的时候涉及到代码膨胀的潜在问题也将会消失。
虚函数的开销
人们一提到虚函数,首先想到的是多态,紧接着想到的就是开销(至少我开始的时候就是这样子的),那么虚函数的开销来自哪里?开销究竟有多大?
在理论上来讲,虚函数所带来的动态开销主要依赖于3个方面:编译器、操作系统和机器。但是在现实中,几乎所有的编译器都以同样的方式操作。调用一个虚函数的开销主要来自于2个方面,一个是如果虚函数不是内联的,就要增加一些额外的机器指令,不过一般来说也就增加3-5个机器指令(是从那里看的,既不清楚了,不过这个结论我倒是记得很清楚),从时间上来讲,与一个非虚拟函数相比,也就多花费10%-20%的开销,如果有几个参数,这个比例会更小。同时,函数调用的连接开销通常只是总的开销的一小部分,所以基本上来讲,序函数的开销可以忽略不计。当然,如果你在程序中大量运用虚拟函数,效率自然就会有很大的下降,积少成多么。
虚析构函数的时机
析构函数能不能是虚拟的,这个问题大家都比较清楚:能!那么什么时候把析构函数设计成虚拟的呢?
初学者往往会犯这样的错误:基类析构函数当然应该始终是虚拟的!
看来这个规则比较的容易记住,当然她也有自己的理论基础(虽然分析的不怎么全面):一个类既然可以作为基类,那么他就打算多态的使用,因此就应该将析构函数设成虚拟的;另外由于目前大多数编译器都采用vtbl的手法实现虚拟函数,而vtbl是一个类共享的,所有的类的实例都共享这个vtbl,换句话说,如果一个类里面已经有了一个虚函数,那么把析构函数声明为一个虚函数不会对每个实例对象有什么空间开销。
不过这样的分析还是不准确,例如下面的例子
class 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的?