什么是多態?
多態就是不同對象對同一行為會有不同的狀態。(舉例 : 學生和成人都去買票時,學生會打折,成人不會)
實現多態有兩個條件: 一是虛函數重寫,重寫就是用來設置不同狀態的
二是對象調用虛函數時必須是指針或者引用
ps:沒有這兩個條件無法構成多態,很多筆試題都會利用這個陷阱讓你上當!
實際上,代碼上體現(動態)多態就是當父類指針指向子類對象,然后通過父類指針能調用子類的成員函數。
什么是虛函數?什么是重寫?
虛函數是帶有virtual關鍵字的成員函數
子類有個和父類完全相同(函數名,形參,返回值都相同,協變和析構函數除外)的虛函數,就稱子類虛函數重寫父類虛函數
多態的原理?
多態是用虛函數表實現的。
有虛函數的類都會生成一個虛函數表,這個表在編譯時生成。
虛函數表是一個存儲虛函數地址的數組,以NULL結尾。
如果要生成子類虛表,就要經過三個步驟:第一步,將父類虛表內容拷貝到子類虛表上;
第二步,將子類重寫的虛函數覆蓋掉表中父類的虛函數;
第三步,如果子類有新增加的虛函數,按聲明次序加到最后
多態如何調用?
滿足多態的函數調用,程序運行起來后,根據對象中的虛表指針來找實際應該調用的函數; 而不滿足多態的函數在函數編譯時就確定函數地址了。

動態綁定與靜態綁定?
靜態綁定是程序編譯時確定程序行為。
動態綁定是程序運行時根據具體的對象確定程序行為。
繼承中的多態:
單繼承無虛函數覆蓋: 虛函數按聲明順序存放,父類虛函數在子類虛函數前面.

單繼承有虛函數覆蓋: 覆蓋的f()替代原有父類虛函數位置,沒覆蓋的不變

多繼承無虛函數覆蓋: 每個父類都有自己的虛表,子類成員函數被放入第一個父類表中

多繼承有虛函數覆蓋: 三個父類虛表中的f()都會被子類函數指針覆蓋

多繼承規則: 多繼承子類未重寫的虛函數放在第一個繼承父類部分的虛函數表中,繼承的虛表都會覆蓋
重復繼承: B類數據重復,具有二義性.


二義性舉例: d.ib = 0; x d.B1::ib = 0; √
菱形虛擬繼承: 解決重復繼承的數據重復,二義性問題.
虛繼承: 繼承語法中加入virtual關鍵字.
虛繼承子類: 加入新的虛函數,會生成一個虛函數指針(vptr)以及一張虛表,放在對象內存最前面.
普通繼承子類: 加入新的虛函數,直接擴展父類虛表.
虛繼承子類會單獨保留父類的vptr和虛表,用一個四字節0分界.
虛繼承子類有一個四字節指針偏移值.
虛基類表: 虛繼承會生成一個隱藏的虛基類指針(vbptr),虛基類指針總是在虛表指針之后.
vbptr指向虛基類表,虛基類表記錄了vbptr到vptr的偏移值.
虛基類表實際上就是記錄了虛表指針的位置.

單虛繼承下,vbptr記錄了兩個vptr所在位置的偏移值.(這里[4]的地址為007C FE00,打印粗心)

菱形虛擬繼承舉例:

內存結構:


菱形虛擬繼承內存分布總結:
(1)基類出現順序: B1(最左父類),B2(次左父類),B(虛祖父類)
(2)D類數據成員在B類前,並以0x00分割
(3)D類覆蓋擴展原則與前面多繼承規則一樣
(4)B類內容放到了最后
如果出現菱形繼承,B1有一個構造函數初始化B,B2也有一個構造函數初始化B,那么D類應該按誰的構造方式初始化B呢?

答案是必須讓D來初始化虛基類B.
C++規定必須由最終的派生類D來初始化虛基類B,直接派生類 B1 和 B2 對 B 的構造函數的調用是無效的,並且虛基類始終優先調用,與聲明位置無關.
虛函數和虛表在哪里?
虛函數和普通函數一樣在代碼段,vs2013測試下,虛表在只讀常量區。


抽象類?
有純虛函數的類。
純虛函數就是虛函數后面再加上 = 0;
體現了接口繼承,只聲明不實現 --- 比如,動物呼吸不能實現,但繼承它的魚和人都能呼吸並且呈現多態性
final --- 讓父類虛函數不能被重寫 --- 體現實現繼承

override --- 純虛函數 + override --- 強制重寫

面試題:
1.inline函數可以是虛函數嗎?
不能,因為inline函數沒有地址,無法放到虛函數表中
2.靜態成員可以是虛函數嗎?
不能, 因為靜態成員函數沒有this指針, 因為有this指針才能訪問到虛表指針,有虛表指針才能找到虛表從而調用實際應該調用的函數。
3.構造函數可以是虛函數嗎?/虛函數指針在什么時候生成的的?
不能,因為對象中的虛表指針是在構造函數初始化列表階段才初始化的
4.析構函數可以是虛函數嗎?什么場景下析構函數是虛函數?
可以,並且最好把基類的析構函數定義成虛函數
當父類指針指向子類對象時,如果析構函數不是虛函數,析構就只會釋放父類對象,造成內存泄漏。(因為析構重名,只能調用一個,調用默認的父類析構函數)
定義成虛函數后,調用析構時就會取出虛表指針找到實際應該調用的函數。(指針雖然都是父類類型,但是指針內取出的虛表是不一樣的,所以析構能調用子類析構)
5.對象訪問普通函數快還是虛函數更快?
首先如果是普通對象,是一樣快的,如果是指針對象或者是引用對象,則調用的普通函數快,因為普通對象在編譯時就確定地址了,虛函數構成多態,運行時調用虛函數需要到虛函數表中去查找
6.下面輸出是?
class B{};
class B1 :public virtual B{};
class B2 :public virtual B{};
class D : public B1, public B2{};
int main()
{
B b;
B1 b1;
B2 b2;
D d;
cout << "sizeof(b)=" << sizeof(b)<<endl; //1,空類用了一個占位符
cout << "sizeof(b1)=" << sizeof(b1) << endl; //4,有虛基類表指針(32位系統)
cout << "sizeof(b2)=" << sizeof(b2) << endl; //4,同上
cout << "sizeof(d)=" << sizeof(d) << endl; //8,有b1和b2兩個虛基類表指針
return 0;
}
7.設計一個不能被繼承的類
//解法:私有一個輔助類,子類虛繼承輔助類,並是他的友元.
// 友元能夠調用輔助類的私有函數
// 因為有虛繼承,所以Try類必須自己構造虛基類,但是虛基類私有了構造和析構,所以調用不了
class MakeSealed{
friend SealedClass;
private:
MakeSealed();
~MakeSealed();
};
class SealedClass : virtual public MakeSealed;
{
public:
SealedClass();
~SealedClass();
};
class Try : public MakeSealed;
{
};
c++的多態總結:
在父類的函數前加上virtual關鍵字,在子類中重寫該函數,運行時將會根據對象的實際類型來調用相應的函數,如果對象類型是子類,就調用子類的函數,如果對象類型是父類,就調用父類的函數。
-
- 虛函數機制(virtual function) , 用以支持執行期綁定,實現多態。
- 虛基類 (virtual base class) ,虛繼承關系產生虛基類,用於在多重繼承下保證基類在子類中擁有唯一實例。
