虛函數的作用
多態性是面向對象程序設計的一個重要特征。顧名思義,多態性就是一個事物具有多種形態。在面向對象方法中一般是這樣表述多態性的:向不同的對象發送同一個消息,
不同的對象在接收時會產生不同的行為。也就是說,每個對象可以用自己的方式去響應共同的消息,所謂消息,就是調用函數,不同的行為就是指不同的實現,即執行不同的函
數。
在C++中,多態性表現形式之一是:具有不同功能的函數可以用同一個函數名,這樣就可以實現用一個函數名調用不同內容的函數。從系統實現的角度來看,多態性分為兩
類:靜態多態性和動態多態性。靜態多態性是通過函數重載實現。由函數重載和運算符重載(運算符重載實質上也是函數重載)形成的多態性屬於靜態多態性,要求程序在編譯
時就知道函數調用的全部信息,因此,在程序編譯時系統就能決定要調用的是哪個函數。靜態多態性的函數調用速度快、效率高,但是缺乏靈活性,在程序運行之前就已經決定
了執行的函數和方法。動態多態性是通過虛函數實現的。特點是:不在編譯時確定調用的哪個函數,而是在程序運行過程中動態地確定操作所針對的對象。這裡先介紹動態多態
性,靜態多態性以後再介紹。
1 虛函數的作用
在同一個類中是不能定義兩個名字相同、參數個數和類型都相同的函數的,否則就是“重復定義”。但在類的繼承層次結構中,在不同的層次中可以出現名字相同、參數個數和類
型都相同而功能不同的函數,這種情況是合法的,因為它們不在同一個類中,編譯系統按照同名覆蓋的原則決定調用的對象。那麼,能否用同一個調用形式來調用派生類和基類
的同名函數。在程序中不是通過不同的對象名去調用不同派生層次中的同名函數,而是通過指針來調用,要做的只是在調用前臨時給指針變量賦予不同的值(使之指向不同的類
對象)。C++中的虛函數就是用來解決動態多態問題的。所謂虛函數,就是在基類聲明函數時虛擬的,並不是實際存在的函數,然後在派生類中才正式定義此函數。在程序運行
期間,用指針指向某一派生類對象,這樣就能調用指針指向的派生類對象中的函數,而不會調用其他派生類中的函數。虛函數的作用是允許在派生類中重新定義與基類同名的函
數,並且可以通過基類指針或引用來訪問基類和派生類中的同名函數。
using namespace std; class Student { public: Student() { } ~Student() { } Student(int num, std::string name, float score) :m_num(num), m_name(name), m_score(score) { } void display() //輸出數據成員 num name score { cout << "num = " << m_num << "; name = "<< m_name.c_str() << "; score = "<< m_score << std::endl; } protected: int m_num; string m_name; float m_score; }; class Graduate : public Student { public: Graduate(int num, std::string name, float score, float wage) :m_wage(wage) { m_num = num; m_name = name; m_score = score; } ~Graduate() { } void display() //輸出成員函數 num name score wage { cout << "num = " << m_num << "; name = " << m_name.c_str() << "; score = " << m_score << "; wage = "<運行結果如下:display(); pt = &grad; //基類指針變量指向派生類對象grad pt->display(); return 0; }
我們本希望輸出grad的全部數據成員,但結果卻不是這樣,這是因為:本來,基類指針是用來指向基類對象的,如果用它指向派生類對象,則自動進行指針類型轉換,將派
生類對象的指針先轉換為基類的指針,這樣基類指針指向的是派生類中基類部分,所以只輸出從基類繼承過來的數據成員。Ofcourse,想要輸出grad1的全部數據成員,可以通
過對象名或指向派生類對象的指針變量來調用display() 。但是,如果該基類有多個派生類,每個派生類又產生新的派生類,每個派生類都有同名函數display,若在程序中需要
調用不同類的同名函數,則上述方法就很不方便。用虛函數就能解決這個問題。只需對原程序作一點修改,在Student類中聲明display函數時,在最前面加一個關鍵字virtual:
1: virtual void display() ;
這樣就把Student類的display函數聲明為虛函數.其余部分不變,這次的運行結果是:
2 在什麼情況下應當聲明虛函數
在使用虛函數時,有兩點需要注意:
(1)只能用virtual聲明類的成員函數,把它作為虛函數,而不能將類外的普通函數聲明為虛函數。
(2)一個成員函數被聲明為虛函數後,在同一類族中的類就不能在定義一個非virtual的但與該虛函數具有相同的參數和函數返回值類型的同名函數。是否應該把一個成員函數聲明為虛函數,主要考慮以下幾點:
a、首先看成員函數所在的類是否會作為基類。然後看成員函數在類的繼承後功能是否會改變,如果希望更改其功能,一般應該將它聲明為虛函數。
b、 如果成員函數在類被繼承後功能無須更改,或派生類用用不到該函數,則不要把它聲明為虛函數。
c、應考慮對成員函數的調用是通過對象名還是通過基類指針或引用去訪問,如果是通過基類指針或引用去訪問,則應當聲明為虛函數。
d、有時,在定義虛函數時,並不定義其函數體,即函數體為空。它的作用只是定義了一個虛函數名,具體功能留給派生類去添加。
3 虛析構函數
析構函數的作用是在對象撤銷之前做必要的“清理現場”的工作。當派生類類的對象從內存中撤銷時一般先調用派生類的析構函數,然後再調用基類的析構函數。
但是,如果用一個基類指針指向一個用new建立的派生類的臨時對象,在程序中用delete運算符撤銷該對象時,會發生這樣一種情況:系統只會執行基類的析構函數,而不會執行派生類的析構函數。
例如:
using namespace std; class Point //定義基類Point類 { public: Point(){} //Point類的構造函數 ~Point() { //Point類的析構函數 cout << " executing Point destructor " << endl; } }; class Circle : public Point //定義派生類Circle類 { public: Circle(){} //Circle類的構造函數 ~Circle() //派生類的析構函數 { cout << " executing Circle destructor " << endl; } }; int main() { Point* p = new Circle; //用new開辟動態存儲空間 delete p; //用delete釋放動態存儲空間 return 0; }
在本程序中,p是指向基類的指針變量,指向用new開辟的動態存儲空間,希望用delete釋放p所指向的空間。但運行結果為:
運行後內存未完全釋放,如下圖所示
表示只執行了基類Point的析構函數,而沒有執行派生類的構造函數。如果希望執行派生類Circle的析構函數,可以將基類的析構函數聲明為虛函數,如下:
virtual ~Point()
{
cout << " executing Point destructor " << endl ;
}
則執行結果為:
運行後內存完全釋放
先調用了派生類的析構函數,再調用基類的析構函數。即當基類的析構函數為虛函數時,無論指針指的是同一類族中的哪一個類對象,系統都會采用動態關聯,調用相應的析構函數,對該對象進行清理工作。
如果將基類的析構函數聲明為虛函數時,由該基類所派生的所有派生類的析構函數都自動成為虛函數,即使派生類的析構函數與基類的析構函數的名字不相同。最好把基類的析構函數聲明為虛函數。
這樣將使所有派生類的析構函數自動成為虛函數。這樣,如果在程序中顯示的調用了delete運算符准備刪除一個對象,則系統會調用相應類的析構函數。這樣造成內存洩漏。