文章目錄
- 一、多態的概念
- 二、多態的定義實現
- 1. 多態的構成條件
- 1.1 虛函數
- 1.2 虛函數的重寫
- 2. 多態的調用
- 3. 虛函數重寫的其他問題
- 3.1 協變
- 3.2 析構函數的重寫
- 三、override和final關鍵字
- 四、重載/重寫/隱藏的對比
- 五、純虛函數和抽象類
- 六、多態的原理
C++的三大主要特性:封裝(類和對象)、繼承、多態。前兩者我們已經學習過了,今天最后來認識一下“多態”特性。
一、多態的概念
多態,就是“多種形態”,指函數的行為可以有多種形態。多態一般分為編譯時多態(靜態多態)和運行時多態(動態多態)。其中,編譯時多態主要就是之前講的函數重載和函數模板,它們傳不同類型的參數就可以調用不同的函數,通過參數的不同達到不同的形態效果,之所以叫編譯時多態,是因為傳參匹配這一過程是在編譯時期完成的。
運行時多態,具體指一個函數傳不同的對象就會完成不同的行為,達到多種形態。比如寫一個買火車票行為,傳普通人類對象是“全價”,傳學生類對象是“打折”,傳軍人類對象就是“優先”,一個函數(行為),根據傳的對象類型不同,就有不同作用。今天談的“多態”指的主要就是運行時多態。
二、多態的定義實現
1. 多態的構成條件
(運行時)多態是一個繼承體系下的類對象,去調用同一函數,而產生不同的行為。比如,Person類為基類,Student類繼承了Person類,Soldier類繼承了Person類,那么這三類的對象買票行為就有不同的效果。
實現多態還有兩個重要的條件:
- 必須是基類的指針類型或引用類型去調用虛函數。
- 被調用的函數必須是虛函數,并且完成了虛函數的重寫(覆蓋)。
我們依次來說明:
要實現多態效果,首先必須是基類的指針或引用去調用,因為只有這樣才既能指向基類對象又能指向派生類對象(的基類的切片)。
第二,派生類必須對基類的虛函數完成重寫,重寫了,基類和派生類才能有這個函數的不同實現方法,多態的不同形態效果才能達到。
1.1 虛函數
類的成員函數前加關鍵字virtual
修飾,那么這個成員函數被稱為虛函數,非成員函數不能加virtual
修飾。如:
class Person
{
public:virtual void BuyTicket(){cout << "全價" << endl;}
};
虛函數的存在就是為了給多態服務的。
1.2 虛函數的重寫
虛函數的重寫指:派生類中有一個跟基類虛函數“三同”的虛函數,則稱派生類的這個虛函數完成了對基類虛函數的重寫。“三同”指的是,函數名相同、返回類型相同、參數類型。
在派生類中重寫基類虛函數時,派生類的虛函數前可以不加virtual修飾,也可以構成重寫,因為繼承后基類的虛函數在派生類中仍保持虛函數屬性。但是這種寫法不規范,實際使用還是建議派生類的虛函數前寫上virtual(但基類的虛函數前是必須寫virtual的)。不過在考試中也有可能故意埋這個坑,注意判斷。
舉例:
class Person
{
public:virtual void BuyTicket(){cout << "全價" << endl;}
};class Student : public Person
{
public:// 注意保證“三同”virtual void BuyTicket(){cout << "打折" << endl;}
};
2. 多態的調用
利用多態的特性,我們可以模擬出簡單的買火車票行為:傳普通人類對象是“全價”,傳學生類對象是“打折”,傳軍人類對象就是“優先”:
class Person
{
public:virtual void BuyTicket(){cout << "全價" << endl;}
};class Student : public Person
{
public:virtual void BuyTicket(){cout << "打折" << endl;}
};class Soldier : public Person
{
public:virtual void BuyTicket(){cout << "優先" << endl;}
};
但是記得剛才說的多態的第一個條件,必須是基類的指針類型或引用類型去調用虛函數,比如:
3. 虛函數重寫的其他問題
3.1 協變
派生類重寫基類虛函數時,重寫時虛函數返回類型也可以不一樣。基類虛函數可以返回自己或其他基類對象的指針或引用,派生類虛函數可以返回自己或其他派生類對象的指針或引用。這種方式稱為卸變,但是它的實際意義并不大,簡單了解即可。
class Person
{
public:virtual Person* BuyTicket(){cout << "全價" << endl;return nullptr;}
};class Student : public Person
{
public:virtual Student* BuyTicket(){cout << "打折" << endl;return nullptr;}
};
或
class A
{ };class B : public A
{ };class Person
{
public:virtual A* BuyTicket(){cout << "全價" << endl;return nullptr;}
};class Student : public Person
{
public:virtual B* BuyTicket(){cout << "打折" << endl;return nullptr;}
};
3.2 析構函數的重寫
析構函數十分特殊,編譯器會對析構函數的名字進行特殊處理——編譯后析構函數的名稱會統一處理成destructor,再加上析構函數都是沒有返回、沒有參數的。因此,基類和派生類的析構函數總會構成“三同”,如果不加virtual修飾成虛函數,那么基類和析構函數既然是“同名”的,就構成隱藏關系了。所以,基類的析構函數一定要加virtual
修飾成虛函數,派生類析構函數的virtual
可寫可不寫。
比如:
class A
{
public:~A(){cout << "~A()" << endl;}
};class B : public A
{
public:~B(){cout << "~B()" << endl;delete p;}
protected:int* p = new int[10];
};
可見,如果~A()
不加virtual,那么delete p2時只會調用A的析構函數,沒有調用B的析構函數,導致了內存泄漏問題。
這樣就沒問題了。
總而言之,基類的析構函數建議設計為虛函數。
三、override和final關鍵字
C++提供了兩個新的關鍵字:
- override:
C++對虛函數重寫的要求是很嚴格的,但是有時候我們可能粗心沒有滿足多態的要求,但是語法沒問題編譯也不會有問題,只有在運行結果不是預期情況下我們才能發現問題。因此C++11提供了override關鍵字,寫在派生類的想要重寫的虛函數后,幫助用戶檢測是否完成了重寫。
- final
之前繼承提到過,如果一個類不想被繼承,就在類名后加final修飾。除此之外final還有一個功能,如果我們不想讓基類的虛函數被重寫,就在這個虛函數()后加final修飾。
四、重載/重寫/隱藏的對比
這三個概念比較相近,要注意區別:
函數重載:
- 兩個函數在同一作用域
- 函數名相同,參數的類型或個數不同,返回值可同可不同
虛函數重寫:
- 兩個函數分別在一個繼承體系的基類和派生類中
- 函數名、參數、返回值相同,協變除外
- 兩個函數必須都是虛函數
隱藏:
- 兩個函數分別在一個繼承體系的基類和派生類中
- 函數名相同
- 兩個函數只要不構成重寫,就是隱藏關系
- 基類和派生類的成員變量相同也構成隱藏關系
五、純虛函數和抽象類
如果在一個虛函數的()后寫上= 0
,則這個虛函數稱為純虛函數,純虛函數不需要定義實現(可以實現但是沒有意義),只要聲明即可。
包含純虛函數的類稱為抽象類,抽象類不能實例化出對象,如果一個抽象類的派生類繼承后不重寫純虛函數,則派生類也是抽象類。可以認為,純虛函數一定程度上強制派生類重寫虛函數,因為不重寫不實例化出對象。
六、多態的原理
還是看一開始的例子:
class Person
{
public:virtual void BuyTicket(){cout << "全價" << endl;}
};class Student : public Person
{
public:virtual void BuyTicket(){cout << "打折" << endl;}
};class Soldier : public Person
{
public:virtual void BuyTicket(){cout << "優先" << endl;}
};
int main()
{Person per;Student stu;Soldier sol;Person* ptr;ptr = &per;ptr->BuyTicket(); ptr = &stu;ptr->BuyTicket();ptr = /ptr->BuyTicket();return 0;
}
調試觀察,可以發現:
每一個對象的第一個成員都是一個叫__vfptr的指針(有些平臺可能會把它放在最后一個),這個指針叫做虛函數表指針,指向這個類的虛函數表。一個含有虛函數的類中都至少有一個虛函數表指針,一個類所有虛函數的地址都會存在一個虛函數表中,虛函數表實際上就是函數指針數組,也稱虛表。
- 基類和每個派生類都有自己獨立的虛函數表。派生類會繼承基類的虛函數表指針,但是這個虛函數表指針和基類的虛函數表指針不是同一個指針,指向的虛表也就不是同一個。
- 派生類重寫了基類的虛函數后,派生類的虛表中對應的原基類虛函數就會被覆蓋成派生類重寫的虛函數地址。這也是為什么重寫也可以稱為覆蓋。
基于這樣的原理,基類的指針或引用調用基類和派生類的同一個虛函數時,其實就是從它們各自的__vfptr找到各自的虛函數表,再找到這個虛函數。但由于派生類的虛函數表中這個虛函數已經被重寫(覆蓋),調用后也就有不同的結果了。這就是多態實現的原理。
本篇完,感謝閱讀。