文章目錄
- 1.多態的概念
- 1.1概念
- 2.多態的定義及實現
- 2.1多態的構成條件
- 2.2虛函數
- 2.3虛函數的重寫
- 2.4 C++11 override 和 final
- 2.5 重載、覆蓋(重寫)、隱藏(重定義)的對比
- 3. 抽象類
- 3.1概念
- 3.2接口繼承和實現繼承
- 4.多態的原理
- 4.1虛函數表
- 4.2多態原理分析
- 4.3 動態綁定與靜態綁定
- 5.單繼承和多繼承關系的虛函數表
- 5.1 單繼承中的虛函數表
- 5.2 多繼承中的虛函數表
- 5.2.1 對象模型
- 5.2.2 原理分析
- 5.3菱形繼承、菱形虛擬繼承
1.多態的概念
1.1概念
多態的概念:通俗來說,就是多種形態,具體點就是去完成某個行為,當不同的對象去完成時會產生出不同的狀態。
舉個例子:比如買票這個行為,當普通人買票時,是全價買票;學生買票時,是半價買票;軍人買票時是優先買票。
2.多態的定義及實現
2.1多態的構成條件
多態是在不同繼承關系的類對象,去調用同一函數,產生了不同的行為。比如Student繼承了
Person。Person對象買票全價,Student對象買票半價。
那么在繼承中要構成多態還有兩個條件:
- 必須通過基類的指針或者引用調用虛函數
- 被調用的函數必須是虛函數,且派生類必須對基類的虛函數進行重寫
2.2虛函數
虛函數:即被virtual修飾的類成員函數稱為虛函數。
class Person {
public:virtual void BuyTicket() { cout << "買票-全價" << endl; }
};
2.3虛函數的重寫
虛函數的重寫(覆蓋):派生類中有一個跟基類完全相同的虛函數(即派生類虛函數與基類虛函數的返回值類型、函數名字、參數列表完全相同),稱子類的虛函數重寫了基類的虛函數。
class Person {
public:virtual void BuyTicket() { cout << "買票-全價" << endl; }
};
class Student : public Person {
public:virtual void BuyTicket() { cout << "買票-半價" << endl; }//注意:在重寫基類虛函數時,派生類的虛函數在不加virtual關鍵字時,//雖然也可以構成重寫(因為繼承后基類的虛函數被繼承下來了在派生//類依舊保持虛函數屬性),但是該種寫法不是很規范,不建議這樣使用
};
void Func(Person& p)
{p.BuyTicket();
}
int main()
{Person ps;Student st;Func(ps);Func(st);return 0;
}
虛函數重寫的兩個例外:
- 協變(基類與派生類虛函數返回值類型不同)
派生類重寫基類虛函數時,與基類虛函數返回值類型不同。即基類虛函數返回基類對象的指針或者引用,派生類虛函數返回派生類對象的指針或者引用時,稱為協變。
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;}
};
- 析構函數的重寫(基類與派生類析構函數的名字不同)
如果基類的析構函數為虛函數,此時派生類析構函數只要定義,無論是否加virtual關鍵字,都與基類的析構函數構成重寫,雖然基類與派生類析構函數名字不同。雖然函數名不相同,看起來違背了重寫的規則,其實不然,這里可以理解為編譯器對析構函數的名稱做了特殊處理,編譯后析構函數的名稱統一處理成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;
return 0;
}
2.4 C++11 override 和 final
從上面可以看出,C++對函數重寫的要求比較嚴格,但是有些情況下由于疏忽,可能會導致函數名字母次序寫反而無法構成重載,而這種錯誤在編譯期間是不會報出的,只有在程序運行時沒有得到預期結果才來debug會得不償失,因此:C++11提供了override和final兩個關鍵字,可以幫助用戶檢測是否重寫
- final:修飾虛函數,表示該虛函數不能再被重寫
class Car
{
public:
virtual void Drive() final {}
};
class Benz :public Car
{
public:
virtual void Drive() {cout << "Benz-舒適" << endl;}
};
- override: 檢查派生類虛函數是否重寫了基類某個虛函數,如果沒有重寫編譯報錯
class Car{
public:
virtual void Drive(){}
};
class Benz :public Car {
public:
virtual void Drive() override {cout << "Benz-舒適" << endl;}
};
2.5 重載、覆蓋(重寫)、隱藏(重定義)的對比
3. 抽象類
3.1概念
在虛函數的后面寫上 =0 ,則這個函數為純虛函數。包含純虛函數的類叫做抽象類(也叫接口類),抽象類不能實例化出對象。派生類繼承后也不能實例化出對象,只有重寫純虛函數,派生類才能實例化出對象。純虛函數規范了派生類必須重寫,另外純虛函數更體現出了接口繼承。
class Person {
public:virtual void BuyTicket() = 0;
};
class Student : public Person {
public:virtual void BuyTicket() { cout << "買票-半價" << endl; }
};
class soldier :public Person
{virtual void BuyTicket() { cout << "買票-優先" << endl; }
};
void Func(Person& p)
{p.BuyTicket();
}
int main()
{soldier ps;Student st;Func(ps);Func(st);return 0;
}
3.2接口繼承和實現繼承
普通函數的繼承是一種實現繼承,派生類繼承了基類函數,可以使用函數,繼承的是函數的實現。虛函數的繼承是一種接口繼承,派生類繼承的是基類虛函數的接口目的是為了重寫,達成多態,繼承的是接口。所以如果不實現多態,不要把函數定義成虛函數。
4.多態的原理
4.1虛函數表
class Base
{
public:virtual void Func1(){cout << "Func1()" << endl;}
private:int _b = 1;
};
int main()
{Base b;cout << sizeof(b);return 0;
}
我們可以得知結果為8
通過觀察測試我們發現b對象是8bytes,除了_b成員,還多一個__vfptr放在對象的前面(注意有些平臺可能會放到對象的最后面,這個跟平臺有關),對象中的這個指針我們叫做虛函數表指針(v代表virtual,f代表function)。一個含有虛函數的類中都至少都有一個虛函數表指針,因為虛函數的地址要被放到虛函數表中,虛函數表也簡稱虛表,。那么派生類中這個表放了些什么呢?我們接著往下分析
class Base
{
public:virtual void Func1(){cout << "Func1()" << endl;}virtual void Func2(){cout << "Base::Func2()" << endl;}void Func3(){cout << "Base::Func3()" << endl;}
private:int _b = 1;
};
class Derive : public Base
{
public:virtual void Func1(){cout << "Derive::Func1()" << endl;}
private:int _d = 2;
};
int main()
{Base b;Derive d;return 0;
}
監視窗口我們得到
1.虛表里存的是虛函數的地址,被重寫的虛函數在相應的虛表下地址會改變
2.派生類對象d中也有一個虛表指針,d對象由兩部分構成,一部分是父類繼承下來的成員,虛表指針也就是存在部分的另一部分是自己的成員
3.虛函數表本質是一個存虛函數指針的指針數組,一般情況這個數組最后面放了一個nullptr。
4.總結一下派生類的虛表生成:a.先將基類中的虛表內容拷貝一份到派生類虛表中 b.如果派生類重寫了基類中某個虛函數,用派生類自己的虛函數覆蓋虛表中基類的虛函數 c.派生類自己新增加的虛函數按其在派生類中的聲明次序增加到派生類虛表的最后
5.虛函數和普通函數一樣是存在代碼段的,只是他的指針又存到了虛表中。另外對象中存的不是虛表,存的是虛表指針,我們看虛表的地址和虛函數的地址是比較相像的,所以虛表也是存在代碼段的
4.2多態原理分析
class Person
{
public:virtual void BuyTicket() { cout << "買票-全價" << endl; }
};
class Student : public Person
{
public:virtual void BuyTicket() { cout << "買票-半價" << endl; }
};
void Func(Person& p)
{p.BuyTicket();
}
int main()
{
//第一張圖測試Person Mike;Func(Mike);Student Johnson;Func(Johnson);
//第二張圖測試Person Mike;Student Johnson;Func(Mike);Johnson.BuyTicket();return 0;
}
- 觀察上圖的淺藍色箭頭我們看到,p是指向mike對象時,p->BuyTicket在mike的虛表中找到虛函數是Person::BuyTicket。
- 觀察上圖的深藍色箭頭我們看到,p是指向johnson對象時,p->BuyTicket在johson的虛表中找到虛函數是Student::BuyTicket。
- 這樣就實現出了不同對象去完成同一行為時,展現出不同的形態
4.再通過上面的匯編代碼分析,看出滿足多態以后的函數調用,不是在編譯時確定的,是運行起來以后到對象的中取找的。不滿足多態的函數調用時編譯時確認好的。
4.3 動態綁定與靜態綁定
- 靜態綁定又稱為前期綁定(早綁定),在程序編譯期間確定了程序的行為,也稱為靜態多態,
比如:函數重載 - 動態綁定又稱后期綁定(晚綁定),是在程序運行期間,根據具體拿到的類型確定程序的具體
行為,調用具體的函數,也稱為動態多態。
5.單繼承和多繼承關系的虛函數表
需要注意的是在單繼承和多繼承關系中,下面我們去關注的是派生類對象的虛表模型,因為基類的虛表模型前面我們已經看過了,沒什么需要特別研究的
5.1 單繼承中的虛函數表
class Base {
public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }
private:int a;
};
class Derive :public Base {
public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }virtual void func4() { cout << "Derive::func4" << endl; }
private:int b;
};
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{// 依次取虛表中的虛函數指針打印并調用。調用就可以看出存的是哪個函數cout << " 虛表地址>" << vTable << endl;for (int i = 0; vTable[i] != nullptr; ++i){printf(" 第%d個虛函數地址 :0X%x,->", i, vTable[i]);VFPTR f = vTable[i];f();}cout << endl;
}
int main()
{Base b;Derive d;PrintVTable((VFPTR*)(*(int*)&b));// 思路:取出b、d對象的頭4bytes,就是虛表的指針,前面我們說了虛函數表本質是一個存虛函數指針的指針數組,這個數組最后面放了一個nullptr
// 1.先取b的地址,強轉成一個int*的指針
// 2.再解引用取值,就取到了b對象頭4bytes的值,這個值就是指向虛表的指針
// 3.再強轉成VFPTR*,因為虛表就是一個存VFPTR類型(虛函數指針類型)的數組。
// 4.虛表指針傳遞給PrintVTable進行打印虛表
// 5.需要說明的是這個打印虛表的代碼經常會崩潰,因為編譯器有時對虛表的處理不干凈,虛表最后面沒有放nullptr,導致越界,這是編譯器的問題。我們只需要點目錄欄的-生成-清理解決方案,再
編譯就好了。PrintVTable((VFPTR*)(*(int*)&d));
}
觀察下圖中的監視窗口中我們發現看不見func3和func4。這里是編譯器的監視窗口故意隱藏了這兩個函數,也可以認為是他的一個小bug。那么我們如何查看d的虛表呢?下面我們使用代碼打印出虛表中的函數。
5.2 多繼承中的虛函數表
5.2.1 對象模型
class Base1 {
public:virtual void func1() { cout << "Base1::func1" << endl; }virtual void func2() { cout << "Base1::func2" << endl; }
private:int b1=1;
};class Base2 {
public:virtual void func1() { cout << "Base2::func1" << endl; }virtual void func2() { cout << "Base2::func2" << endl; }
private:int b2=2;int bb=2;
};class Derive : public Base1, public Base2 {
public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }
private:int d1=3;
};// 用程序打印虛表
typedef void(*VF_PTR)();//void PrintVFTable(VF_PTR table[])
void PrintVFTable(VF_PTR* table)
{for (int i = 0; table[i] != nullptr; ++i){printf("[%d]:%p->", i, table[i]);VF_PTR f = table[i];f();}cout << endl;
}int main()
{Derive d;PrintVFTable((VF_PTR*)(*(int*)&d));//PrintVFTable((VF_PTR*)(*(int*)((char*)&d+sizeof(Base1))));//不理解就看下面的對象模型Base2* ptr2 = &d;PrintVFTable((VF_PTR*)(*(int*)(ptr2)));return 0;
}
觀察下圖可以看出:多繼承派生類的未重寫的虛函數放在第一個繼承基類部分的虛函數表中,但是我們同樣發現個問題,重寫的func1()地址不一樣,這是為什么呢?接下來看原理分析
5.2.2 原理分析
class Base1 {
public:virtual void func1() { cout << "Base1::func1" << endl; }virtual void func2() { cout << "Base1::func2" << endl; }
private:int b1=1;
};class Base2 {
public:virtual void func1() { cout << "Base2::func1" << endl; }virtual void func2() { cout << "Base2::func2" << endl; }
private:int b2=2;int bb=2;
};class Derive : public Base1, public Base2 {
public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }
private:int d1=3;
};// 用程序打印虛表
typedef void(*VF_PTR)();//void PrintVFTable(VF_PTR table[])
void PrintVFTable(VF_PTR* table)
{for (int i = 0; table[i] != nullptr; ++i){printf("[%d]:%p->", i, table[i]);VF_PTR f = table[i];f();}cout << endl;
}int main()
{Derive d;Base1* ptr1 = &d;Base2* ptr2 = &d;ptr1->func1();ptr2->func1();return 0;
}
至于為什么要偏移呢?
假設我們這時候有個指針p,
Derive *p=&d
p調用func1()是合理的無可厚非,ptr1調用func1()就也是合理的,因為ptr1和p指向的是同一個位置,那么問題解決掉了,ptr2就是要偏移到p指針的位置才可以調用func1()。
5.3菱形繼承、菱形虛擬繼承
class A
{
public:virtual void func1(){}
public:int _a;
};class B : virtual public A
{
public:virtual void func1(){}virtual void func2(){}
public:int _b;
};class C : virtual public A
{
public:virtual void func1(){}virtual void func3(){}
public:int _c;
};class D : public B, public C
{
public:virtual void func1(){}
public:int _d;
};int main()
{D d;d.B::_a = 1;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5;return 0;
}
簡單了解下模型