參考資料:阿秀
一、面向對象三大特性
封裝:將數據和代碼捆綁在一起,避免外界干擾和不確定性訪問
繼承:讓某種類型對象獲得另一個類型對象的屬性和方法
多態:同一種事務表現出不同事務的能力,即:向不同對象發送同一消息,不同的對象在接收時會產生不同的行為
????????重載實現編譯時多態,虛函數實現運行時多態。
實現多態的兩種方式:
- 覆蓋:子類重新定義父類的虛函數做法
- 重載:允許存在多個同名函數,而這些函數的參數表不同(參數個數不同、或者參類型不同、或者兩者都不同)
二、虛函數
????????在基類的函數前加上virtual關鍵字,在派生類中重寫該函數,運行時將會根據所指對象的實際類型來調用相應的函數,如果對象類型是派生類,就調用派生類的函數,如果對象類型是基類,就調用基類的函數。
底層原理:
- 虛表: 虛函數表的縮寫,類中含有virtual關鍵字修飾的方法時,編譯器會自動生成虛表
- 虛表指針: 在含有虛函數的類實例化對象時,對象地址的前四個字節存儲的指向虛表的指針
上圖展示了虛表和虛表指針在基類對象和派生類對象中的模型,那么多態具體是如何實現的呢?
1. 對象初始化
- 編譯器會自動為每個含有虛函數的類生成一份虛表,該表時一個一維指針數組,虛表中保存了虛函數的入口地址。
- 編譯器會在每個對象的前四個字節中保存一個虛表指針vptr,指向對象所屬類的虛表。在構造時,根據對象的類型初始化虛指針vptr,從而讓虛指針指向正確的虛表。
- 在派生類定義對象時,程序會自動調用構造函數,在構造函數中創建虛表并對虛表初始化。
2. 虛指針指向
- 當派生類對基類的虛函數沒有重寫時,派生類的虛表指針指向的是基類的虛表;
- 當派生類對基類的虛函數重寫時,派生類的虛表指針指向的是自身的虛表;
- 當派生類中有自己的虛函數時,在自己的虛表指針中將此虛函數地址添加在后面。
這樣指向派生類的基類指針在運行時,可以根據派生類對虛函數重寫情況動態進行調用,從而實現多態性。
構造函數和析構函數可以聲明為虛函數嗎?
????????構造函數不能定義為虛函數,析構函數可以為虛函數,并且一般情況下基類析構函數都要定義為虛函數。
????????構造函數:每個含有虛函數的類都有一個虛表指針,指向虛函數表。如果構造函數時虛函數,就需要通過虛表指針尋找虛函數表,從而找到對應的虛函數實現。但是類對象還沒有初始化,就沒有虛表指針,找不到虛函數,所以構造函數不能時虛函數。
????????析構函數:只有在基類析構函數是虛函數時,調用delete操作符銷毀指向派生類的基類指針時,才能準確調用派生類的析構函數,而派生類的析構函數又自動調用基類的析構函數,這樣整個派生類的對象完全被釋放。
三、純虛函數
虛函數和純虛函數的區別?
- 虛函數是為了實現動態編譯產?的,目的是通過基類類型的指針指向不同對象時,自動調用相應的、和基類同名的函數(使?同?種調用形式,既能調用派生類又能調用基類的同名函數)。虛函數需要在基類中加上 virtual修飾符修飾,因為virtual會被隱式繼承,所以子類中相同函數都是虛函數。當?個成員函數被聲明為虛函數之后,其派生類中同名函數自動成為虛函數,在派生類中重新定義此函數時要求函數名、返回值類型、參數個數和類型全部與基類函數相同。
- 純虛函數只是相當于?個接口名,但含有純虛函數的類不能夠實例化。
純虛函數首先是虛函數,其次沒有函數體,取而代之使用“=0”代替。
它的函數指針會被存在虛函數表中,由于純虛函數并沒有具體的函數體,因此他在虛函數表中的值為0,其他有函數體的虛函數則是函數的具體地址。
一個類中如果存在純虛函數,稱為抽象類,抽象類不能用于實例化,一般用于定義一些公有方法。子類繼承抽象類也必須實現其中的純虛函數才能實例化對象。
四、虛擬繼承
一個類可以從多個基類(父類)繼承屬性和行為。在C++等支持多重繼承的語言中,一個派生類可以同時擁有多個基類。
多重繼承可能引入一些問題,如萎形繼承問題,比如當一個類同時繼承了兩個擁有相同基類的類,而最終的派生類又同時繼承了這兩個類時,可能導致二義性和代碼設計上的復雜性。為了解決這些問題,C++ 提供了虛繼承,通過在繼承聲明中使用 virtual 關鍵字,可以避免在派生類中生成多個基類的實例,從而解決了菱形繼承帶來的二義性。
舉個🌰:
#include <iostream>using namespace std;class A{}class B : virtual public A{};class C : virtual public A{};class D : public B, public C{};int main(){cout << "sizeof(A):" << sizeof A <<endl; // 1,空對象,只有?個占位cout << "sizeof(B):" << sizeof B <<endl; // 4,?個bptr指針,省去占位,不需要對?cout << "sizeof(C):" << sizeof C <<endl; // 4,?個bptr指針,省去占位,不需要對?cout << "sizeof(D):" << sizeof D <<endl; // 8,兩個bptr,省去占位,不需要對?}
上述代碼所體現的關系是,B和C虛擬繼承A,D公有繼承B和C,這種方式是?種菱形繼承或者鉆石繼承,可以用下圖來表示:
? ? ? ? 虛擬繼承的情況下,無論基類被繼承多少次,只會存在一個實體。
????????虛擬繼承基類的子類中,子類會增加某種形式的指針,或者指向虛基類子對象,或者指向一個相關表格;表格中存放的不是虛基類子對象的地址,就是其偏移量,此類指針被稱為bptr。如果即存在vptr又存在bptr,某些編譯器會將其優化,合并為一個指針。