【C++進階十】多態深度剖析
- 1.多態的概念及條件
- 2.虛函數的重寫
- 3.重寫、重定義、重載區別
- 4.C++11新增的override 和final
- 5.抽象類
- 6.虛表指針和虛表
- 6.1什么是虛表指針
- 6.2指向誰調用誰,傳父類調用父類,傳子類調用子類
- 7.多態的原理
- 8.單繼承的虛表狀態
- 9.多繼承的虛表狀態
- 10.菱形繼承
- 11.菱形虛繼承
1.多態的概念及條件
繼承是實現多態的前提
通俗來說,多態就是多種狀態,父子對象完成相同任務會產生不同的結果
例如:成年人買火車票是全價票,學生買票是折扣票
在繼承中構成多態要有兩個條件:
- 必須通過父類的指針或引用去調用
- 被調用的函數必須是虛函數,且完成虛函數的重寫
2.虛函數的重寫
什么是虛函數:
被virtual修飾的類成員函數稱為虛函數
什么是虛函數的重寫(覆蓋):
三同:父子虛函數的函數名、返回值類型和參數相同
(參數的缺省值可以不同)
傳遞不同的對象調用不同的函數
傳父類調用的是父類的虛函數
傳子類調用的是子類的虛函數
虛函數重寫的例外:
-
子類的虛函數可以不加virtual
重寫體現了接口繼承:子類把函數的聲明繼承下來,重寫的是函數的實現,所以不寫virtual也可以滿足多態的條件
-
協變:子類與父類的虛函數返回值類型不同,但必須滿足父類虛函數返回父類對象的指針或引用,子類虛函數返回子類對象的指針或引用
這個父子類指針也可以是自己的父子類,也可以是指其他類型的父子類:
子類的虛函數可以不加virtual可以防止析構函數出錯:
正確使用析構函數:
父類和子類的虛函數的析構函數函數名并不相同卻依然構成虛函數的重寫,因為析構函數在多態中會被編譯器修改成同一個名字
為什么會變成同一個名字:析構函數會因為父子類關系,在子類調用析構后會自動調用父類的析構,如果父子的析構函數不是虛函數,調用析構時就不會調用子類的析構,即使我們指向了子類
為什么沒有調用到子類:因為delete的內部構成是:析構函數和operator delete(),而operator delete 有一個特點就是調用delete的指針是什么類型的,就會調用什么類型的析構函數,上圖的兩個指針p1和p2都是父類person類型,所以就都調用了父類person的析構函數,沒有調動子類的析構函數
實際使用結果如下:
所以想要指向父類調用父類析構,指向子類調用子類析構,就需要編譯器把析構函數的名字進行了統一,滿足了虛函數重寫的三同:父子虛函數的函數名、返回值類型和參數相同
子類可以不寫virtual,只要父類加上了virtual就可以進行虛函數的重寫,但是不太建議
3.重寫、重定義、重載區別
4.C++11新增的override 和final
overrride:檢查子類虛函數是否重寫了父類的某個虛函數,如果沒有重寫編譯報錯
final:修飾虛函數,表示該虛函數不能被重寫
5.抽象類
在虛函數后面寫上=0,這個虛函數被稱為純虛函數,包含純虛函數的類叫做抽象類,抽象類不能實例化對象
若創建一個子類繼承抽象類,則該子類也包含純虛函數,子類也會變成抽象類,所以子類創建對象也會報錯
6.虛表指針和虛表
6.1什么是虛表指針
sizeof(Base) 大小是多少?
以結構體的內存對齊考慮,在32位機器下,大小應為8字節,但是實際上為12字節
_vfptr代表虛函數表指針
加上虛表指針,內存對齊后字節大小為12
6.2指向誰調用誰,傳父類調用父類,傳子類調用子類
- 父類對象的虛表與子類對象的虛表沒有任何關系,這是兩個不同的對象
- 通過虛表指針和虛函數表就可以實現多態性,即可以在運行時確定應該調用哪個類的虛函數
- 虛表指針是類級別的,而不是函數級別的
- 每個類只有一個虛表指針,指向其對應的虛函數表,但是多繼承的時候,就會可能有多張虛表
- 每一個類中的虛函數在虛表中都有地址
如圖所示, 圖中父子類各自的虛表指針指向的虛表以及虛表內部的地址并不相同,這證明了父類的虛表指針和子類的虛表指針指向的虛表并不是同一個
7.多態的原理
class Person
{
public:virtual void BuyTicket(){cout << "成人:全價票" << endl;}
};class Student : public Person
{
public:virtual void BuyTicket() {cout << "學生:半價票" << endl;}
};void func(Person* p)
{p->BuyTicket();//父類指針指向父類對象A(或子類對象B)
}int main()
{Person A;func(&A);//傳父類對象Student B;func(&B);//傳子類對象return 0;
}
父類對象內的虛函數放進了父類的虛表,子類對象繼承了父類的同時也繼承了父類的虛表,如果子類有虛函數的重寫,那么父類的虛表內有父類的虛函數,子類的虛表內有子類的虛函數(重寫后的虛函數)
指向誰調用誰,父類指針指向父類對象,則從父類的虛表中找到父類的虛函數;父類指針指向子類對象,則從子類的虛表中找到子類的虛函數,因此產生了指向父類調用父類,指向子類調用子類的現象
不是多態則是在編譯時就是已經指向了某個地方,而不是因為指向誰調用了誰
多態的本質在底層看來就是在虛表內尋找虛函數
虛函數和普通函數一樣都是存在于代碼段中的,而不是存在虛表內部的,虛表內部僅僅儲存了虛函數的地址,而虛表儲存在常量區上
虛函數表 本質是一個虛函數指針數組
子類的虛表是由父類的虛表拷貝過來的,再向其中填入新的地址,所以造成覆蓋
為什么父類對象不可以實現多態,必須是父類的指針或引用?
若為父類對象,就會把子類中屬于父類的那一部分拷貝給父類,有可能把子類的虛表也拷貝給父類,若拷貝成功,則父類對象的虛表就不知道是父類的虛表還是子類的虛表了
若為指針或者引用,將子類中屬于父類那一部分切出來,使指針指向屬于父類那一部分,或者作為屬于父類那一部分的別名,子類的虛表還是子類的
8.單繼承的虛表狀態
class A
{
public:virtual void func1(){cout << "A::func1" << endl;}virtual void func2(){cout << "A::func2" << endl;}
};class B : public A
{
public:virtual void func1(){cout << "B::func1" << endl;}virtual void func3(){cout << "B::func3" << endl;}virtual void func4(){cout << "B::func4" << endl;}
};int main()
{A a;B b;return 0;
}
可以看到子類的虛表不正常,因為每個類中的虛函數在虛表中都要出現,而子類虛表里少了兩個虛函數的地址,func1是重寫的,func2是繼承的(沒有重寫),而func3和func4不見了,子類自己的虛函數消失了
這里可以解釋為一種bug:是Visual Studio監視窗口的bug
也可也理解為:子類的虛表實際上是拷貝了父類的虛表,重寫的部分進行覆蓋,沒有重寫的部分原模原樣地拷貝,可以說是子類的虛表被隱藏了
9.多繼承的虛表狀態
class A
{
public:virtual void func1(){cout << "A::func1" << endl;}virtual void func2(){cout << "A::func2" << endl;}
private:int a;
};class B
{
public:virtual void func1(){cout << "B::func1" << endl;}virtual void func2(){cout << "B::func2" << endl;}
private:int b;
};class C : public A, public B
{
public:virtual void func1(){cout << "C::func1" << endl;}virtual void func3(){cout << "C::func3" << endl;}
private:int c;
};int main()
{C temp;return 0;
}
C由兩個部分構成,第一個是繼承了A, 一個類中只有一個虛表指針,所以A內部有一個虛表指針和一個int類型的對象a,第二個是繼承了B, 一個類中只有一個虛表指針,所以B內部有一個虛表指針和一個int類型的對象b
C類的虛函數func3放到了繼承的第一個類A的虛表內部
所以C中有兩個虛表指針,同時這里的兩個虛表指針不能合二為一,這關系于切片問題
所以多繼承內部可能會有多個虛表指針
10.菱形繼承
class A
{
public:virtual void func1(){cout << "A::func1" << endl;}
private:int a;
};class B : public A
{
public:virtual void func2(){cout << "B::func2" << endl;}
private:int b;
};class C : public A
{
public:virtual void func3(){cout << "C::func3" << endl;}
private:int c;
};class D : public B,public C
{
public:virtual void func4(){cout << "D::func4" << endl;}
private:int a;
};int main()
{D temp;return 0;
}
菱形繼承和多繼承沒區別,同時func4放入了B的虛表內部
D類對象temp有兩張虛表,分別是B和C的虛表
11.菱形虛繼承
class A
{
public:virtual void func1(){cout << "A::func1" << endl;}int _a = 1;
};class B : virtual public A
{
public:virtual void func1(){cout << "B::func1" << endl;}int _b = 2;
};class C : virtual public A
{
public:virtual void func1(){cout << "C::func1" << endl;}int _c = 3;
};class D : public B,public C
{
public:virtual void func1(){cout << "D::func1" << endl;}virtual void func2(){cout << "D::func2" << endl;}int _d = 4;
};int main()
{D temp;return 0;
}
A的虛表由B、C共享
D自己新增的func2需要虛表,但D對象中的B沒有的虛表
虛基表存儲偏移量,幫助B、C找到A
注意:上述的B、C沒有新增額外虛函數,如果有新增,則D的虛表消失,B和C各有一張虛表,D的虛函數放入B的虛表內,共計三張虛表
class A
{
public:virtual void func1(){cout << "A::func1" << endl;}int _a = 1;
};class B : virtual public A
{
public:virtual void func1(){cout << "B::func1" << endl;}virtual void func3(){cout << "B::func3" << endl;}int _b = 2;
};class C : virtual public A
{
public:virtual void func1(){cout << "C::func1" << endl;}virtual void func4(){cout << "C::func4" << endl;}int _c = 3;
};class D : public B,public C
{
public:virtual void func1(){cout << "D::func1" << endl;}virtual void func2(){cout << "D::func2" << endl;}int _d = 4;
};int main()
{D temp;return 0;
}