(根據《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運算符准備刪除一個對象,則系統會調用相應類的析構函數。