C++中的虚函数(virtual function)

 

缺省情况下,类的成员函数是非虚拟的。当一个成员函数为非虚拟的时候,通过一个类对象(指针或引用)而被调用的该成员函数,是该类对象的静态类型中定义的成员函数。

当成员函数是虚拟的时候,通过指针或引用而被调用的该成员函数,是在该类对象的动态类型中被定义的成员函数。但是,正如所发生的,类对象的静态和动态类型是相同的,所以虚拟函数机制只在使用指针和引用时才会如预期般地起作用。

第一次引入虚拟函数的基类时,必须在类声明中指定virtual关键字。如果虚函数的定义放在类的外面,则不能再次指定关键字virtual。假设有下面的类层次:

class A
{
public:
    virtual void foo() { cout << "A::foo() is called" << endl;}
};

class B: public A
{
public:

//备注:只要在基类中已声明为virtual,这里即使不使用virtual关键字,默认也是虚函数

//同样,如果还有从B派生的子类,对应的成员函数也是虚函数
    virtual void foo() { cout << "B::foo() is called" << endl;}
};

那么,在使用的时候,我们可以:
A * a = new B();
a->foo();// 
在这里,a虽然是指向A的指针,但是被调用的函数(foo)却是B
这个例子是虚函数的一个典型应用。所谓虚函数,虚就虚在推迟联编或者动态联编上,一个类函数的调用并不是在编译时刻被确定的,而是在运行时刻被确定的。由于编写代码的时候并不能确定被调用的是基类的函数还是哪个派生类的函数,所以被称为函数。

虚函数只能借助于指针或者引用来达到多态的效果,如果是下面这样的代码,则虽然是虚函数,但它不是多态的:

class A
{
public:
    virtual void foo();
};

class B: public A
{
    virtual void foo();
};

void bar()
{
    A a;
    a.foo();   // A::foo()
被调用
}

 

 

多态(polymorphism)一词最初来源于希腊语polumorphos,含义是具有多种形式或形态的情形。在程序设计领域,一个广泛认可的定义是一种将不同的特殊行为和单个泛化记号相关联的能力。和纯粹的面向对象程序设计语言不同,C++中的多态有着更广泛的含义。除了常见的通过类继承和虚函数机制生效于运行期的动态多态(dynamic polymorphism)外,模板也允许将不同的特殊行为和单个泛化记号相关联,由于这种关联处理于编译期而非运行期,因此被称为静态多态(static polymorphism)【荣耀】 

在了解了虚函数的意思之后,再理解多态的相关概念就比较容易了。仍然针对上面的类层次,但是使用的方法变的复杂了一些:

void bar(A * a)
{
    a->foo();  // 
被调用的是A::foo() 还是B::foo()
}

因为foo()是个虚函数,所以在bar这个函数中,只根据这段代码,无从确定这里被调用的是A::foo()还是B::foo(),但是可以肯定的说:如果a指向的是A类的实例,则A::foo()被调用,如果a指向的是B类的实例,则B::foo()被调用。

多态这么神奇,但是能用来做什么呢?这个命题我难以用一两句话概括,一般的C++教程(或者其它面向对象语言的教程)都用一个画图的例子来展示多态的用途,这里就不再重复了。下面将从抽象的角度描述一下多态的相关思想:

在面向对象的编程中,首先会针对数据进行抽象(确定基类)和继承(确定派生类),构成类层次。这个类层次的使用者在使用它们的时候,如果仍然在需要基类的时候写针对基类的代码,在需要派生类的时候写针对派生类的代码,就等于类层次完全暴露在使用者面前。如果这个类层次有任何的改变(增加了新类),都需要使用者知道(针对新类写代码)。这样就增加了类层次与其使用者之间的耦合。

多态可以使程序员脱离这种窘境。再回头看看上面的例子,bar()作为A-B这个类层次的使用者,它并不知道这个类层次中有多少个类,每个类都叫什么,但是一样可以很好的工作,当有一个C类从A类派生出来后,bar()也不需要知道(修改)。这完全归功于多态--编译器针对虚函数产生了可以在运行时刻确定被调用函数的代码。

如何实现“动态联编”

编译器是如何针对虚函数产生可以在运行时刻确定被调用函数的代码呢?也就是说,虚函数实际上是如何被编译器处理的呢?Lippman在《深度探索C++对象模型》中的不同章节讲到了几种方式,这里把标准的方式简单介绍一下。

这里所说的“标准”方式,也就是所谓的“VTABLE机制。编译器发现一个类中有被声明为virtual的函数,就会为其搞一个虚函数表,也就是VTABLEVTABLE实际上是一个函数指针的数组,每个虚函数占用这个数组的一个slot。一个类只有一个VTABLE,不管它有多少个实例。派生类有自己的VTABLE,但是派生类的VTABLE与基类的VTABLE有相同的函数排列顺序,同名的虚函数被放在两个数组的相同位置上。在创建类实例的时候,编译器还会在每个实例的内存布局中增加一个vptr字段,该字段指向本类的VTABLE。通过这些手段,编译器在看到一个虚函数调用的时候,就会将这个调用改写,针对上面的例子:

void bar(A * a)
{
    a->foo();
}

会被改写为:

void bar(A * a)
{
    (a->vptr[1])();
}

因为派生类和基类的foo()函数具有相同的VTABLE索引,而他们的vptr又指向不同的VTABLE,因此通过这样的方法可以在运行时刻决定调用哪个foo()函数。虽然实际情况远非这么简单,但是基本原理大致如此。

纯虚函数

如下声明表示一个函数为纯虚函数(纯虚函数也可以有定义)(如果一个类里面有一个或多个纯虚函数,这个类就是抽象类)

class A
{
public:
    virtual void foo()=0;   // =0
标志一个虚函数为纯虚函数
};

纯虚函数用来规范派生类的行为,实际上就是所谓的接口。它告诉使用者,我的派生类都会有这个函数。试图创建一个抽象基类的独立类对象会导致编译时刻错误。那么,一般在什么情况下使用纯虚函数呢?

l  当想在基类中抽象出一个方法,且该基类只做能被继承,而不能被实例化;

l  这个方法必须在派生类(derived class)中被实现;

如果满足以上两点,可以考虑将该方法申明为pure virtual function。下面举例说明相关的概念,首先定义一个形状的类(Cshape),但凡是形状我们都要求其能显示自身,所以我们定义了如下的一个类:

class CShape

{

    virtual void Show(){};

};

现实中没有Cshape这样的形状,因此我们不想让CShape这个类被实例化,我们首先想到的是将Show函数的定义(实现)部分删除如下:

class CShape

{

    virtual void Show();//删除实现部分

};

当我们使用下面的语句实例化一个CShape时:

CShape cs;  //这是我们不允许的,但仅用上面的代码是可以通过编译(但link时失败)。

怎么样避免一个CShape被实例化,且在编译时就被发现?答案是使用pure virtual funcion我们再次修改CShape类如下:

class CShape

{

public:

    virtual void Show()=0;

};

这时在实例化CShape时就会有类似于下面这样的报错信息:

error C2259: 'CShape' : cannot instantiate abstract class due to following members:

warning C4259: 'void __thiscall CShape::Show(void)' : pure virtual function was not defined

 

下面再来看看被继承的情况首先定义一个CPoint2D的类,它继承自Cshape必须实现基类(CShape)中定义的Show()方法。

我们最初的本意是让每一个派生自CShape的类,都要实现Show()方法,但是偶尔会忘记在某一个派生类中实现Show()方法,为了避免这种情况,pure virtual funcion发挥作用了,我们看以下代码:

class CPoint2D : public CShape
{
public:
   CPoint2D()
   {
       printf("CPoint2D ctor is invoked/n");
   };
   void Msg()
   {
       printf("CPoint2D.Msg() is invoked/n");
   };

 

 /*---I'm sorry to forget implement the Show()---
   void Show()
   {
       printf("Show() from CPoint2D/n");
   };

----------------------------------------------*/
};

 

void main()
{
    CPoint2D p2d;   //
如果派生类(CPoint2D)没有实现Show(),则编译不通过
    p2d.Msg();

    CShape *pShape = &p2d;
    pShape->Show();

 

    //不能实例化基类
    //CShape cs;
}

如果我们忘记在派生类Cpoint2D中实现Show()方法,在实例化CPoint2D时,编译时会出现类似于下面这样的报错:

error C2259: 'CShape' : cannot instantiate abstract class due to following members:

warning C4259: 'void __thiscall CShape::Show(void)' : pure virtual function was not defined

 

虚析构函数

析构函数也可以是虚的,甚至是纯虚的。例如:
class A
{
public:
    virtual ~A()=0;   // 
纯虚析构函数
};
当一个类打算被用作其它类的基类时,它的析构函数必须是虚的。考虑下面的例子:

class A
{
public:
    A() { ptra_ = new char[10];}
    ~A() { delete[] ptra_;} // 
非虚析构函数
private:
    char * ptra_;
};

class B: public A
{
public:
    B() { ptrb_ = new char[20];}
    ~B() { delete[] ptrb_;}
private:
    char * ptrb_;
};

void foo()
{
    A * a = new B;
    delete a;
}

在这个例子中,程序也许不会象你想象的那样运行,在执行delete a的时候,实际上只有A::~A()被调用了,而B类的析构函数并没有被调用!这是否有点儿可怕?如果将上面A::~A()改为virtual,就可以保证B::~B()也在delete a的时候被调用了。因此基类的析构函数都必须是virtual的。

构造函数和析构函数中的虚函数调用

一个类的虚函数在它自己的构造函数和析构函数中被调用的时候,它们就变成普通函数了,不“虚”了。也就是说不能在构造函数和析构函数中让自己“多态”。

派生类对象中构造函数的调用顺序是,先调用基类的构造函数,然后是派生类的构造函数。例如,当我们定义一个Manger 类对象时,如:

Manger tim( "tim" );

构造函数的调用顺序是先Employee 然后Manger

当执行Employee基类的构造函数时,tim Manger部分还没有被初始化。实际上,tim还不是一个完整的Manger,只有它的Employee子对象被构造了。

如果在基类的构造函数中调用了一个虚拟函数,而基类和派生类都定义了该函数的实例,将会怎么样?应该调用哪一个函数实例?如果可以调用虚拟函数的派生类实例,并且它访问任意的派生类成员,那么调用的结果在逻辑上是未定义的。而程序可能会崩溃。

为了防止这样的事情发生,在基类构造函数中调用的虚拟实例总是在基类中活动的虚拟实例。实际上,在基类构造函数中,派生类对象只不过是一个基类类型的对象而已。

对于派生类对象,在基类析构函数中也是如此:派生类部分也是未定义的,但是,这一次不是因为它还没有被构造,而是因为它已经被销毁。

class A
{
public:
    A() { foo();}        // 
在这里,无论如何都是A::foo()被调用!
    ~A() { foo();}       // 
同上
    virtual void foo();
};

class B: public A
{
public:
    virtual void foo();
};

void bar()
{
    A * a = new B;
    delete a;
}

如果你希望delete a的时候,会导致B::foo()被调用,那么你就错了。同样,在new B的时候,A的构造函数被调用,但是在A的构造函数中,被调用的是A::foo()而不是B::foo()

 

参考资料

C++ primer

《深度探索C++对象模型》

http://www.royaloo.com/articles/articles_2003/PolymorphismInCpp.htm

http://blog.chinaunix.net/u/11680/showart_351509.html

http://www.vckbase.com/document/viewdoc/?id=939

http://www.vckbase.com/document/viewdoc/?id=950

http://baike.baidu.com/view/161302.htm

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值