天天看點

多态性(二)——動态多态性之虛函數

  1.虛函數的作用

  C++中的虛函數是用于解決動态多态性的問題。所謂虛函數(virtual function),就是在基類聲明函數是虛拟的,并不是實際存在的函數,然後在派生類中才正式定義此函數。

那麼虛函數有何作用呢?我們先來看看這樣一段程式:

在上一篇讨論靜态多态性的文章裡,讓我們在其中的Circle類和Cylinder類中都增加一個函數void display();

在Circle類中:

void Circle::display()

{cout<<"Center=["<<x<<","<<y<<"],r="<<radius<<endl;}

在Cyclinder類中:

void Cylinder::display()

{cout<<"Center=["<<x<<","<<y<<"],r="<<radius<<",h="<<height<<endl;}

下面是測試程式:

#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!"<<endl;} 則測試程式的結果如下:

多态性(二)——動态多态性之虛函數

  把基類中的析構函數定義為虛函數後,則由其派生的所有派生類的析構函數都自動成為虛函數,即便基類的析構函數與派生類的析構函數不同名。   [注:構造函數不能聲明為虛函數。這是因為在執行制造函數時類對象還未完成建立過程,就談不上把函數與類對象的綁定。] 參考資料:《C++程式設計(第2版)》.譚浩強.清華大學出版社.2011.8.pag398-405

繼續閱讀