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