多態
簡介: 面向對象的三大特性之一,多態顧名思義即具有多種形態,即去執行某個行為時,當不同的對象去執行時會產生不同的狀態
構成多態的條件
條件一
必須通過基類(父類)的指針或者引用調用虛函數(函數被virtual所修飾)
tips:父類的指針或引用要指向或引用子類對象
virtual void test(){}
條件二
被調用的函數必須是虛函數,且派生類必須對基類的虛函數進行重寫
tips:破壞任意一個條件都會導致無法構成多態
- 虛函數的重寫是接口繼承(普通函數的重寫是實現繼承)
ps:普通函數的重寫是外殼不同,將函數體(實現)繼承下來 - 子類中的虛函數只是對父類的接口的一個聲明(聲明必須保持一致,故函數名,形參以及返回值都相同),重寫只是將父類函數的“外殼”拿了下來,然后再在這個“外殼”內填充函數體(重寫的是實現)
滿足上述兩個條件即構成多態:即通過基類(父類)的指針或者引用調用虛函數
子類沒重寫也會進行運行時決議(多態),但是由于沒有進行覆蓋,仍舊調用的是父類的虛函數
虛函數重寫/覆蓋的條件
虛函數+三同(函數名,形參以及返回值都相同),不符合重寫的條件即構成隱藏
-
tips1:子類重寫虛函數時,是否添加virtual修飾對多態不影響
-
tips2:重寫的協變,協變即返回值可以不同,但要求父子函數的返回值必須分別是父子關系的指針或者引用
如下述兩種方式都是可以構成多態(此種用途并不多,了解即可)class Person { public://virtual void BuyTicket(char) { cout << "買票-全價" << endl; }/*virtual Person* BuyTicket(int) { cout << "買票-全價" << endl;return this;}*///假設A是B的父類,即下述也構成多態virtual A* BuyTicket(int){cout << "買票-全價" << endl;return nullptr;} };class Student : public Person { public:// 虛函數重寫/覆蓋條件 : 虛函數 + 三同(函數名、參數、返回值)// 不符合重寫,就是隱藏關系// 特例1:子類虛函數不加virtual,依舊構成重寫 (實際最好加上)// 特例2:重寫的協變。返回值可以不同,要求必須時父子關系的的指針或者引用/*virtual Student* BuyTicket(int){cout << "買票-半價" << endl;return this;}*/virtual B* BuyTicket(int){ cout << "買票-半價" << endl;return nullptr;} };
構成多態的原理
虛表(虛函數表)
-
虛表內存儲著所有的虛函數(函數地址),虛表本質是一個數組,數組內存著的都為函數指針
- 父類對象和子類對象里各自都有各自的虛表
子類對象的虛表是拷貝父類對象得來 - 當子類重寫虛函數時,則修改了子類自己的虛表對應的函數(覆蓋成子類重寫后的函數)
- 父類對象和子類對象里各自都有各自的虛表
虛表指針
當類內存在了virtual修飾的虛函數,則該類內會默認生成一個虛表指針(__vfptr)指向一張虛表
虛表指針在vs環境下,默認是在對象的頭4個字節或者頭8個字節(可以通過取出該字節的內容所指向的地址來打印虛表)
ps: 虛函數表是編譯時即生成的,在構造函數中進行初始化虛表指針,對象中存儲的為虛表指針,虛表存儲位置大致在常量區(編譯階段即生成好了)
-
tips:可以按下述方式嘗試打印虛表內的內容
class Person { public:virtual void BuyTicket() { cout << "Person::買票-全價" << endl;}virtual void Func1(){cout << "Person::Func1()" << endl;} };class Student : public Person { public:virtual void BuyTicket() { cout << "Student::買票-半價" << endl;}virtual void Func2(){cout << "Student::Func2()" << endl;} };typedef void(*VFPTR)();//void PrintVFTable(VFPTR table[]) //void PrintVFTable(VFPTR* table, size_t n) void PrintVFTable(VFPTR* table) {//vs下,虛表末尾會加上空指針作為標識for (size_t i = 0; table[i] != nullptr; ++i)//for (size_t i = 0; i < n; ++i){printf("vft[%d]:%p->", i, table[i]);//table[i]();VFPTR pf = table[i];pf();}cout << endl; } int main() {// 同一個類型的對象共用一個虛表Person p1;Person p2;// vs下 不管是否完成重寫,子類虛表跟父類虛表都不是同一個Student s1;Student s2;//取到對象頭四個字節的虛表指針中的函數地址,再強轉成函數指針(因為本身就是函數地址)PrintVFTable((VFPTR*)*(int*)&s1);PrintVFTable((VFPTR*)*(int*)&p1); }
普通多繼承下的情況
- 多繼承中,子類新增的虛函數會被放到多繼承下來的第一個對象的虛表里
多繼承的對象有幾個,則子類中有多少個虛表(從父類繼承得來)- 多繼承下,子類自身的虛函數會被放到多繼承第一個對象的虛表中
- 多繼承下,不同的父類指針指向子類對象并調用虛函數時,底層實現略有不同,不過到底也是相同的
因為調用函數時,本質也要傳入指向對象的地址,繼承的兩個基類所在的地址不同,故此底層跳轉步驟不盡相同- 如果是第一個繼承的對象,則是直接進行call函數地址然后jump到函數實現
- 如果是第二個繼承的對象,則會先call指令,然后會先偏移到子類對象的首地址處,再進行jump
菱形繼承下的情況
-
最開始菱形繼承中,如果菱形繼承的兩個父類沒有額外的虛函數,則是共用基類的虛表(通過虛基表指向)
-
如果菱形繼承下,兩個父類還有額外的虛函數,則父類其還會擁有自己的虛表指針(指向虛表)
-
虛基表:虛繼承中產生的虛基類表(解決數據冗余和二義性)
- 虛基表的記錄的內容其一是當前派生類的虛基表與其虛表的偏移量(如果不存在額外的虛表則偏移量為0)
為了讓派生類能夠找到其虛表的位置 - 其二是記錄虛基類與其派生類在當前對象模型中的偏移地址
- 虛基表的記錄的內容其一是當前派生類的虛基表與其虛表的偏移量(如果不存在額外的虛表則偏移量為0)
-
只要是虛函數,函數地址都會放入虛表中,無論是否被重寫,子類的虛表中既有父類的虛函數,也有子類的虛函數
tips:同一個類型的對象共用一個虛表,(vs環境)子類和父類的虛表不管是否完成重寫,二者虛表都不是同一個
總結
多態的本質即當符合多態的兩個條件,調用時則會到指向對象的虛表中找到對應的函數地址進行調用
故此多態的調用時運行時才通過虛表確定了函數的地址,編譯時并不知道會自身會指向父類還是子類的對象,運行到了才會到實際指向對應對象的虛表內找到函數地址再進行調用
(普通函數的調用是調用call指令,在編譯鏈接時確定了函數的地址(聲明+定義),運行時直接調用)
析構函數的重寫
父類的析構函數在繼承中建議添加virtual修飾,完成虛函數的重寫
class Person{public:virtual ~Person(){cout << "~Person()" << endl;}
}
class Student{public:virtual ~Student(){cout << "~Student()" << endl;}
}
int main(){Person* ptr1 = new Person();delete ptr1;//如果不構成多態,則是什么類型即調用什么類型的析構函數,不符合預期Person* ptr2 = new Student();delete ptr2;//構成多態則指向父類調用父類析構,指向子類調用子類析構,子類析構后再自動調用父類的析構函數
}
- 編譯器生成的析構函數的作用(同上)
自己調用自己的析構,父類對象去調用父類的析構- 由于多態的需要,析構函數的名字會被統一處理成destructor()
所以析構函數也會與父類構成隱藏 - 由于語法與編譯器要求,構造時需要先構造父類,再構成子類,析構則需要保證先析構子類,再析構父類,所以自定義析構函數時不需要顯式調用父類的析構,編譯器會在析構完子類后自動調用父類的析構
- 由于多態的需要,析構函數的名字會被統一處理成destructor()
override和final關鍵字
- 當一個虛函數不想被重寫,則使用final進行修飾(使用場景極少)
- override用于修飾子類的虛函數,其對子類的虛函數是否重寫進行了強制性語法檢查
重載、覆蓋(重寫)、隱藏(重定義)的對比
- 重載
- 兩個函數在同一作用域
- 函數名相同,參數不同(個數,類型,順序)
- 重寫(覆蓋)
- 兩個函數分別在基類和派生類的作用域
- 函數名,參數,返回值都必須相同(協變屬于例外)
- 兩個函數必須都是虛函數(用virtual修飾)
- 重定義(隱藏)
- 兩個函數分別在基類和派生類的作用域
- 函數名相同
- 基類與派生類的同名函數不構成重寫即為重定義(隱藏)
抽象類
在虛函數后面寫上=0,則這個函數為純虛函數,包含這個純虛函數的類即為抽象類(也被成為接口類)
- 抽象類基類是無法實例化出對象的
- 子類繼承了純虛類后,子類必須得進行虛函數的重寫,否則也無法實例化對象