多态性(二)——动态多态性之虚函数

  1.虚函数的作用

  C++中的虚函数是用于解决动态多态性的问题。所谓虚函数(virtual function),就是在基类声明函数是虚拟的,并不是实际存在的函数,然后在派生类中才正式定义此函数。

那么虚函数有何作用呢?我们先来看看这样一段程序:
在上一篇讨论静态多态性的文章里,让我们在其中的Circle类和Cylinder类中都增加一个函数void display();

在Circle类中:

void Circle::display()

{cout<<"Center=["<


在Cyclinder类中:
void Cylinder::display()

{cout<<"Center=["<


下面是测试程序:
#include
   
   
    
    
#include"Point.h"
#include"Circle.h"
#include"Cylinder.h"
using namespace std;

int main()
{
    Circle a(1.1,1.1,1.1);
    Cylinder b(2.2,2.2,2.2,2);
    Circle *pt=&a;
    pt->display();
    pt=&b;
    pt->display();
return 0;
}

   
   

得出结果如下:

  可以从结果中看出,结果中并没有把Cylinder类的对象b的全部数据输出(缺少了height),这是因为当使pt指向对象b时,再调用pt->display()时,并没有如想象中那样调用了b中的display函数,而是调用了a中的display函数,这就是同名覆盖原则下编译的效果。(在同一个类中是不能定义两个名字相同、参数个数和类型都相同的函数,否则就是“重复定义”。但由于有类的继承,所以这些“完全同名函数”可以在不同的类里出现。编译时,编译系统会按照同名覆盖的原则决定调用的对象。)


  [相关说明:基类指针(pt)是用来指向基类对象的,如果用它指向派生类对象,则自动进行指针类型转换,将派生类的对象的指针先转换为基类的指针,这样,基类指针指向的是派生类对象中的基类部分。所以,如不修改程序,是无法通过基类指针去调用派生类对象中的成员函数的。]

  如果想调用对象b中的display函数,可以新定义一个指向Cylinder类对象的指针变量,再使该新的指针变量指向Cylinder类的对象。但是,如果一个基类派生出多个基类,每个派生类又派生出多个派生类,形成了同一基类的类族,而每个派生类都有同名函数,要想在程序中调用同一类族的不同类的同名函数,就要定义大量的指向各派生类的指针变量,这会很麻烦。而虚函数的应用可以很好地解决这一问题。

  现在让我们在Circle类中的display函数的定义前加上关键字virtual,如下:

  virtual void display();


  再编译一下测试程序,得出结果如下:

 

  由程序运行结果我们可以看出:用同一种调用方式,用同一个指针变量(pt是指向基类对象的指针变量),可以调用同一类族中不同类的虚函数,这就是虚函数实现的动态多态性的体现:同一类族中不同类的对象,对同一函数调用作出不同的响应。


  总结:虚函数的作用:允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数。

(注:在基类中定义的非虚函数会在派生类中被重新定义,如果用基类指针调用该成员函数,则系统会调用对象中基类部分的成员函数;如果用派生类指针调用该成员函数,则系统会调用派生类对象中的成员函数,而这并不是多态性行为的。)

  在一个类里,函数重载处理的是同一层次上的同名函数问题;而虚函数处理是不同层次(多个类)上的同名函数问题。前者是横向,后者是纵向。同一类族的虚函数的首部是相同的,而函数重载时函数的首部是不同的(参数个数或类型不同)。


  2.虚函数的用法
(1)在基类中用virtual声明成员函数为虚函数,在类外定义虚函数时不必再加virtual。
(2)在派生类中重新定义此函数,函数名、函数类型、函数参数个数和类型必须与基类的虚函数相同,根据派生类的需要重新定义函数体;
若在派生类中没有对基类的虚函数重新定义,则派生类简单地继承其直接基类的虚函数;
当一个成员函数被声明为虚函数后,其派生类中的同名函数都自动成为虚函数(所以在派生类中,该函数的virtual可加可不加)。
(3)定义一个指向基类对象的指针变量,并使它指向同一类族中需要调用该函数的对象。
(4)通过该指针变量调用此虚函数,此时调用的就是指针变量指向的对象的同名函数。

 

 3.声明虚函数的注意事项

(1)只能用virtual声明类的成员函数,把它作为虚函数,而不能将类外的普通函数声明为虚函数。

(2)一个成员函数被声明为虚函数后,在同一类族中的类就不能再定义一个非virtual的但与该虚函数具有相同的参数(包括个数和类型)和函数返回值类型的同名函数。


 4.相关概念

  关联(blinding):确定调用的具体对象的过程。

  函数重载和通过对象名调用的虚函数,在编译时即可确定其调用的虚函数属于一类,其过程称为静态关联(static binding),又称早期关联(early binding)

  而另一种通过基类指针调用虚函数,由于并没有指定对象名,而编译时只能作静态的语法检查,则只从词句形式上是无法确定调用对象的。在这种情况下,编译系统把它放到运行阶段处理,在运行阶段确定关联关系。在运行阶段,基类指针变量先指向了某一个类对象,然后通过此指针变量调用该对象中函数。此时,调用哪一个对象的函数无疑是很确定的。这种过程称为动态关联(dynamic binding),也称滞后关联(late binding)

  使用虚函数时,系统要有一定的空间开销。当一个类带有虚函数时,编译系统会为该类构造一个虚函数表(virtual function table,简称vtable),它是一个指针数组,存放每个虚函数的入口地址。系统在进行动态关联时的时间开销是很少的,因此,虚函数多态性是高效的。


 5.虚析构函数

 下面下来看一个例子:

我们先定义一个Father类:

#ifndef FATHER
#define FATHER
#include
   
   
    
    
using namespace std;

class Father
{public:
Father(){}
virtual ~Father(){cout<<"I have deleted Father!"<
    
    
   
   

然后再定义一个派生类Kid类:

#ifndef KID
#define KID
#include
    
    
     
     
using namespace std;

class Kid:public Father
{public:
Kid(){}
~Kid(){cout<<"I have deleted Kid!"<
     
     
    
    
下面是测试程序:

#include
     
     
      
      
#include"Father.h"
#include"Kid.h"
using namespace std;


void main()
{
    Father *p=new Kid;
    delete p;
}

     
     
测试结果如下图:


  在测试结果中我们可以发现,系统只执行基类的析构函数,而不执行派生类的析构函数。原因是前文我们所说的同名覆盖原则之下的编译。

  所以我们要把基类的析构函数定义为虚函数,以使编译系统在撤销对象时,不会发生没有执行派生类的析构函数。

在Fateher类的析构函数前加上关键字virtual,如下:

virtual ~Father(){cout<<"I have deleted Father!"<

则测试程序的结果如下:


  把基类中的析构函数定义为虚函数后,则由其派生的所有派生类的析构函数都自动成为虚函数,即便基类的析构函数与派生类的析构函数不同名。

  [注:构造函数不能声明为虚函数。这是因为在执行制造函数时类对象还未完成建立过程,就谈不上把函数与类对象的绑定。]


参考资料:《C++程序设计(第2版)》.谭浩强.清华大学出版社.2011.8.pag398-405

你可能感兴趣的:(c++,多态,虚函数,c++)