C++学习之路—多态性与虚函数(一)利用虚函数实现动态多态性

时间:2022-04-22 21:57:28

(根据《C++程序设计》(谭浩强)整理,整理者:华科小涛,@http://www.cnblogs.com/hust-ghtao转载请注明)

    多态性是面向对象程序设计的一个重要特征。顾名思义,多态性就是一个事物具有多种形态。在面向对象方法中一般是这样表述多态性的:向不同的对象发送同一个消息,不同的对象在接收时会产生不同的行为。也就是说,每个对象可以用自己的方式去响应共同的消息,所谓消息,就是调用函数,不同的行为就是指不同的实现,即执行不同的函数。在C++中,多态性表现形式之一是:具有不同功能的函数可以用同一个函数名,这样就可以实现用一个函数名调用不同内容的函数。

    从系统实现的角度来看,多态性分为两类:静态多态性和动态多态性。

    静态多态性是通过函数重载实现。由函数重载和运算符重载(运算符重载实质上也是函数重载)形成的多态性属于静态多态性,要求程序在编译时就知道函数调用的全部信息,因此,在程序编译时系统就能决定要调用的是哪个函数。静态多态性的函数调用速度快、效率高,但是缺乏灵活性,在程序运行之前就已经决定了执行的函数和方法。

   动态多态性是通过虚函数实现的。特点是:不在编译时确定调用的哪个函数,而是在程序运行过程中动态地确定操作所针对的对象。

    这里先介绍动态多态性,静态多态性以后再介绍。

1    虚函数的作用

    在同一个类中是不能定义两个名字相同、参数个数和类型都相同的函数的,否则就是“重复定义”。但在类的继承层次结构中,在不同的层次中可以出现名字相同、参数个数和类型都相同而功能不同的函数,这种情况是合法的,因为它们不在同一个类中,编译系统按照同名覆盖的原则决定调用的对象。

    那么,能否用同一个调用形式来调用派生类和基类的同名函数。在程序中不是通过不同的对象名去调用不同派生层次中的同名函数,而是通过指针来调用,要做的只是在调用前临时给指针变量赋予不同的值(使之指向不同的类对象)。C++中的虚函数就是用来解决动态多态问题的。所谓虚函数,就是在基类声明函数时虚拟的,并不是实际存在的函数,然后在派生类中才正式定义此函数。在程序运行期间,用指针指向某一派生类对象,这样就能调用指针指向的派生类对象中的函数,而不会调用其他派生类中的函数。虚函数的作用是允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数。

    例子如下:

   1: class Student
   2: {
   3: public:
   4:     ...
   5:     void display () ;             //输出数据成员 num name score 
   6: protected:
   7:     int num ;
   8:     string name ;
   9:     float score ;
  10: };
  11:  
  12: class Graduate : public Student
  13: {
  14: public: 
  15:     ... 
  16:     void display () ;             //输出成员函数 num name score wage 
  17: private:
  18:     float wage ;
  19: };
  20:  
  21: int main()
  22: {
  23:     Student stud1 ( 1001 ,"Li" , 87.5 ) ;            //定义基类对象
  24:     Graduate grad1 ( 2001 , "Wang" , 98.5 , 1200 ) ; //定义派生类对象
  25:     Student* pt = &stud1 ;                           //定义基类指针变量,指向stud1
  26:     pt->display() ;
  27:     pt = &grad1 ;                                   //基类指针变量指向派生类对象grad1
  28:     pt->display() ;
  29:  
  30:     return 0 ;
  31: }

运行结果如下:

num:1001

name:Li

score:87.5

 

num:2001

name:Wang

score:98.5

 

我们本希望输出grad1的全部数据成员,但结果却不是这样,这是因为:本来,基类指针是用来指向基类对象的,如果用它指向派生类对象,则自动进行指针类型转换,将派生类对象的指针先转换为基类的指针,这样基类指针指向的是派生类中基类部分,所以只输出从基类继承过来的数据成员。Ofcourse,想要输出grad1的全部数据成员,可以通过对象名或指向派生类对象的指针变量来调用display() 。但是,如果该基类有多个派生类,每个派生类又产生新的派生类,每个派生类都有同名函数display,若在程序中需要调用不同类的同名函数,则上述方法就很不方便。

    用虚函数就能解决这个问题。只需对原程序作一点修改,在Student类中声明display函数时,在最前面加一个关键字virtual:

   1: virtual void display() ;

这样就把Student类的display函数声明为虚函数.其余部分不变,这次的运行结果是:

 

num:1001

name:Li

score:87.5

 

num:2001

name:Wang

score:98.5

wage:1200

 

现在用同一个指针变量,不但输出了stud1的全部数据,还输出了grad1的全部数据,说明已经成功调用了grad1的display函数。在基类中的display被声明成虚函数,在声明派生类时被重载,这时派生类的同名函数display就取代了基类中的虚函数。因此在使用基类指针指向派生类对象后,调用display函数时就调用了派生类的display函数。

    虚函数的以上功能很有实际意义。在面向对象的程序设计中,经常会用到类的继承,目的是保留基类的特性,以减少新类的开发时间。但是,从基类继承而来的某些成员函数不完全适应派生类的需要,当把基类的某个成员函数声明为虚函数后,允许在派生类中对该函数重新定义,赋予它新的功能,并且可以通过指向基类的指针指向同一类族中不同类的对象,从而调用其中的同名函数。注意:当一个成员函数被声明为虚函数后,其派生类中的同名函数都自动成为虚函数,但为使程序更加清晰,习惯上在每层声明该函数时都加virtual。

 

2    在什么情况下应当声明虚函数

    在使用虚函数时,有两点需要注意:

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

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

    是否应该把一个成员函数声明为虚函数,主要考虑以下几点:

    (1)首先看成员函数所在的类是否会作为基类。然后看成员函数在类的继承后功能是否会改变,如果希望更改其功能,一般应该将它声明为虚函数。

    (2)如果成员函数在类被继承后功能无须更改,或派生类用用不到该函数,则不要把它声明为虚函数。

    (3)应考虑对成员函数的调用是通过对象名还是通过基类指针或引用去访问,如果是通过基类指针或引用去访问,则应当声明为虚函数。

    (4)有时,在定义虚函数是,并不定义其函数体,即函数体为空。它的作用只是定义了一个虚函数名,具体功能留给派生类去添加。

 

3    虚析构函数

    析构函数的作用是在对象撤销之前做必要的“清理现场”的工作。当派生类类的对象从内存中撤销时一般先调用派生类的析构函数,然后再调用基类的析构函数。但是,如果用一个基类指针指向一个用new建立的派生类的临时对象,在程序中用delete运算符撤销该对象时,会发生这样一种情况:系统只会执行基类的析构函数,而不会执行派生类的析构函数。

例如:

   1: class Point                        //定义基类Point类
   2: {
   3: public:
   4:     Point(){}                      //Point类的构造函数
   5:     ~Point()
   6:     {                              //Point类的析构函数
   7:         cout << " executing Point destructor " << endl ;
   8:     }
   9: };
  10:  
  11: class Circle : public Point        //定义派生类Circle类
  12: {
  13: public:
  14:     Circle(){}                    //Circle类的构造函数
  15:     ~Circle()                     //派生类的析构函数
  16:     {
  17:         cout << " executing Circle destructor " << endl ;
  18:     }
  19: };
  20:  
  21: int main ()
  22: {
  23:     Point* p = new Circle ;        //用new开辟动态存储空间
  24:     delete p ;                      //用delete释放动态存储空间
  25:     return 0 ;
  26: }

在本程序中,p是指向基类的指针变量,指向用new开辟的动态存储空间,希望用delete释放p所指向的空间。但运行结果为:

 

excuting Point destrcutor

 

表示只执行了基类Point的析构函数,而没有执行派生类的构造函数。如果希望执行派生类Circle的析构函数,可以将基类的析构函数声明为虚函数,如下:

   1: virtual ~Point()
   2:     {
   3:         cout << " executing Point destructor "  << endl ;
   4:     }

则执行结果为:

 

excuting Circle destrcutor

excuting Point destrcutor

 

先调用了派生类的析构函数,再调用基类的析构函数。即当基类的析构函数为虚函数时,无论指针指的是同一类族中的哪一个类对象,系统都会采用动态关联,调用相应的析构函数,对该对象进行清理工作。

   如果将基类的析构函数声明为虚函数时,由该基类所派生的所有派生类的析构函数都自动成为虚函数,即使派生类的析构函数与基类的析构函数的名字不相同。最好把基类的析构函数声明为虚函数。这样将使所有派生类的析构函数自动成为虚函数。这样,如果在程序中显示的调用了delete运算符准备删除一个对象,则系统会调用相应类的析构函数。