面向对象三大特性之一:多态(C++)
目錄
多態的定義及實現
抽象類
多態的原理
單繼承和多繼承關系的虛函數表
多態的定義及實現
1、什么是多態?
當不同的對象去完成某個行為時,會產生出不同的結果。多態是:不同繼承關系的類對象去調用同一函數時,產生了不同的行為。
例如:Student類繼承了Person類。 Person對象買票全價,Student對象買票半價。這就是多態行為。
2、構成多態的兩個必要條件
- 調用函數的對象必須是指針或者引用。?若不用指針或引用,則傳一個對象給基類類型(切片),父類將自己的虛表改為子類的虛表,不合理。
- 被調用的函數必須是虛函數,且完成了虛函數的重寫。
虛函數:在類的成員函數前面加關鍵字virtual。
虛函數的重寫:派生類中有一個跟基類的完全相同虛函數,他們的函數名、參數、返回值都相同,我們就稱子類的虛函數重寫了基類的虛函數。另外虛函數的重寫也叫作虛函數的覆蓋。
實現一個簡單的多態例子:
class Person { public:virtual void BuyTicket(){cout << "買票全價" << endl;} }; class Student : public Person { public:virtual void BuyTicket(){cout << "半價買票" << endl;} };void Func(Person& people) {people.BuyTicket(); }void Test() {Person p;Func(p);Student s;Func(s); }3、重寫
虛函數的重寫中,派生類中重寫的成員函數可以不加virtual關鍵字,因為繼承后基類的虛函數被繼承下來了在派生類依舊保持虛函數屬性,我們只是重寫了它。但這是非常不規范的,我們平時不要這樣使用。
虛函數重寫有一個例外:重寫的虛函數的返回值可以不同,但必須分別是基類指針和派生類指針或者基類引用和派生類引用。這種行為叫做?協變。
//協變舉例 class A{}; class B : public A{};class Person { public:virtual A* f(){return new A;} }; class Student : public Person { public:virtual B* f(){return new B;} };析構函數的重寫問題:若基類的析構函數被寫成了虛函數,那么繼承下來的派生類中是否重寫了析構函數?這里他們看起來函數名不相同,違背了重寫的規則,但其實可以理解為編譯器對析構函數的重寫進行了特殊處理,編譯后析構函數的名稱統一處理成destructor。這也說明了基類的析構函數最好寫成虛函數。
class Person { public: virtual ~Person() { cout << "~Person()" << endl; } }; class Student : public Person { public: virtual ~Student(){ cout << "~Student()" << endl;} };// 只有派生類Student的析構函數重寫了Person的析構函數,下面的delete對象調用析構函數,才能構成多態,才能保證p1和p2指向的對象正確的調用析構函數。 int main() { Person* p1 = new Person; Person* p2 = new Student;delete p1; //這里希望通過對象調析構,所以用虛函數delete p2;//結果為:// ~Person()// ~Student()return 0; }為什么將析構函數寫成虛函數?如果析構函數不使用virtual,使用動態綁定,則在析構的時候就會忽略掉派生類的部分。若我們在派生類中進行了空間的開辟,而在派生類的析構中對其進行釋放,如過不調用派生類析構,會造成內存泄漏。
//基類的析構函數不是虛函數時,兩個析構函數沒有構成多態. //在析構的時候,是根據類型析構,而不是根據對象析構,忽略了派生類的部分,會造成內存泄漏。 class Person { public: ~Person() { cout << "~Person()" << endl; } }; class Student : public Person { public: ~Student(){ cout << "~Student()" << endl;} };int main() { Person* p1 = new Person; Person* p2 = new Student;delete p1; delete p2;//結果為:// ~Person()// ~Person() //只對基類不部分進行了析構,而并沒有對派生類部分進行析構return 0; }4、接口繼承和實現繼承
實現繼承:普通函數的繼承是一種實現繼承,派生類繼承了基類函數,可以使用函數,繼承的是函數的實現。
接口繼承:虛函數的繼承是一種接口繼承,派生類繼承的是基類虛函數的接口,目的是為了重寫,達成多態,繼承的是接口。所以如果不實現多態,不要把函數定義成虛函數。
繼承達到的目的就是實現繼承,多態就是接口繼承。
5、重載、重寫(覆蓋)、重定義(隱藏)的對比?
重載:
- 兩個函數在同一個作用域中。
- 函數名相同,參數個數或類型不同。
重寫(覆蓋):
- 兩個函數在兩個不同的作用域中。
- 函數名,參數,返回值都相同(協變除外)。
- 兩個函數都必須是虛函數。
重定義(隱藏):
- 兩個函數在兩個不同的作用域中。
- 函數名相同。
- 上述條件成立后,如果不是重寫就一定構成了隱藏。
?
抽象類
純虛函數:在虛函數的后面寫上 =0 ,則這個函數為純虛函數。只對函數進行聲明,不實現。在派生類中才對于函數進行實現。
純虛函數的目的:強制重寫虛函數。
抽象類:包含純虛函數的類叫做抽象類(也叫接口類)。
- 抽象類不能實例化出對象,派生類繼承后也不能實例化出對象,
- 當派生類中將繼承的抽象類的純虛函數都重寫實現了,才可以實例化出對象。
- 更體現出了接口繼承。
?
C++11還提供了override 和 ?nal 來修飾虛函數:
override:
虛函數的意義就是實現多態,如果沒有重寫,虛函數就沒有意義。所以C++11中使用了 純虛函數 + override?的方式來強制重寫虛函數。
override 修飾的派生類虛函數沒有重寫會編譯報錯。
final:
final 修飾基類的虛函數不能被派生類重寫 。
?
多態的原理
1、虛函數表(虛表)
虛函數表本質是一個存放 虛函數指針 的指針數組,這個數組最后面放了一個nullptr。虛函數指針就是類中虛函數的地址,這些虛函數的地址存放在這個指針數組中。
類對象中有著一個隱藏的成員指針_vfptr,我們叫做虛函數表指針,這個指針指向虛函數表。
派生類的虛表生成:
? ? ? a.先將基類中的虛表內容拷貝一份到派生類虛表中
? ? ? b.如果派生類重寫了基類中某個虛函數,用派生類自己的虛函數覆蓋虛表中基類的虛函數
? ? ? c.派生類自己新增加的虛函數按其在派生類中的聲明次序增加到派生類虛表的最后
虛函數存在哪??
? ? ? 虛函數和普通函數一樣的,都是存在代碼段的,只是他的指針又存到了虛表中。
虛表存在哪?
? ? ? 在vs下驗證后發現虛表存在代碼段。
虛函數表指針存在哪?
? ? ? 虛函數指針是存放在對象中,所以虛函數指針的位置是跟著對象的位置走的對象在棧上被創建,虛函數指針存在于棧上;對象被創建在堆上,虛函數指針就存在于堆上。
2、多態的原理
實際上的多態就是不同的對象,在調用時查找其虛函數表,找到要調用的函數。
在派生類中,派生類的虛函數表已完成了重寫,所以盡管調用的是同一個函數,但虛表卻不同,完成的是不同的動作,展現出不同的形態。
滿足多態后的函數調用,不是在編譯時確定的,是運行起來以后到對象的中去找的。不滿足多態的函數調用,是編譯時確認好的。
3、動態綁定與靜態綁定?
1)?靜態綁定(前期綁定/早綁定):在程序編譯期間確定了程序的行為,也稱為靜態多態,比如:函數重載
2)?動態綁定(后期綁定/晚綁定):是在程序運行期間,根據具體拿到的類型確定程序的具體行為,調用具體的函數,也稱為動態多態。
?
單繼承和多繼承關系的虛函數表
1、單繼承下的虛函數表
- 派生類的虛函數表先存放拷貝自基類的虛函數表,并用重寫的函數將基類對應的函數覆蓋。在表的后面,按派生類自己的聲明順序,加入自己的虛函數地址。
- 在vs的編譯器下監視的窗口中在虛函數表中無法看到派生類自己的虛函數,可以通過打印虛函數表看到。
- 虛函數表指針存放在對象的前四個字節,以nullptr結尾,相當于一個函數指針數組 。
2、多繼承下的虛函數表
- 多繼承派生類的未重寫的虛函數放在第一個繼承基類部分的虛函數表中
?
?
?
?
?
?
?
?
?
總結
以上是生活随笔為你收集整理的面向对象三大特性之一:多态(C++)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Linux——进程信号(总结)
- 下一篇: Caused by: org.xml.s