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

  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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章