本章目標
1.多態的概念
2.多態的定義實現
3.虛函數
4.多態的原理
1.多態的概念
多態作為面對三大特性之一,它所指代的和它的名字一樣,多種形態.但是這個多種形態更多的指代是函數的多種形態.
多態分為靜態多態和動態多態.
靜態多態在前面已經學習過了,就是函數重載以及模板,它們是在編譯時就已經確定下來了,也被成為編譯時多態.它們通過傳不同的參數實現函數不同的形態.
我們在這里主要將動態多態,也就是運行時多態.當我們運行某個函數的時候,它會根據傳過來的對象的不同,來實現不同的行為,簡單來說就是統一繼承體系下的不同類對象去調用同一個函數產生了不同的行為
2.多態的定義實現
2.1實現多態的條件
1.必須是基類的指針或者引用去調用虛函數
2.虛函數必須完成了重寫或者覆蓋
因為我們前面所將的切片的類型兼容轉換,只有基類的指針或者引用才能即指向基類對象又指向派生類對象.
虛函數的重寫或者覆蓋所指的是它的實現重寫,這樣基類和派生類才能有不同的函數.
才能實現多態.
#include<iostream>
using namespace std;
class A
{
public:virtual void a(){cout << "A" << endl;}
};
class b :public A
{
public:virtual void a(){cout << "b" << endl;}};
int main()
{A* ptr1 = new A;A* ptr2 = new b;ptr1->a();ptr2->a();return 0;
}
以上就是多態的實現.
3.虛函數
類成員函數,在函數的前面加上virtual修飾,我們就稱之為虛函數.非類成員函數是不能用virtual修飾的.
class Person
{public:virtual void BuyTicket() { cout << "買票全價" << endl;}};
3.1虛函數的重寫覆蓋
虛函數的重寫覆蓋所指的是在派生類之中有一個和基類完全的一樣的虛函數(返回值,函數名,參數列表),那么就叫做虛函數的重寫覆蓋.
在有的地方只在基類的虛函數的地方加上virtual,而在派生類中,并沒有加入virtual來進行修飾,這樣也是構成重寫或者覆蓋的.因為從基類繼承下來的虛函數,在派生類也繼承下來了它的虛函數屬性
3.2協變
在派生類重寫基類虛函數的時候,我們可以讓派生的返回類型與基類不同,去返回基類或者派生類的指針或者引用,這個指針或者引用可以是其他類的.
class A {};class B : public A {};class Person {public:virtual A* BuyTicket()
{
cout << "
買票
全價
" << endl;return nullptr;}};class Student : public Person {public:virtual B* BuyTicket()
{
cout << "
買票
打折
" << endl;return nullptr;}};void Func(Person* ptr){ptr->BuyTicket();}int main(){Person ps;Student st;Func(&ps);Func(&st);return 0;}
3.3析構函數的重寫
只要基類的析構函數為虛函數,它的派生類的析構一定會與基類的析構函數構成重寫,在前面我們說繼承的時候講到析構函數會在編譯時統一將名稱處理成destructor,這樣它們就構成了隱藏,而在這里則是構成了重寫.
#include<iostream>
using namespace std;
class A
{
public:virtual void a(){cout << "A" << endl;}virtual ~A(){cout << "~A" << endl;}
};
class b :public A
{
public:virtual void a(){cout << "b" << endl;}~b(){delete[] arr;cout << "~b" << endl;}
private:int* arr = new int[10];
};
int main()
{A* ptr1 = new A;A* ptr2 = new b;ptr1->a();ptr2->a();delete ptr1;delete ptr2;return 0;
}
3.4override和final關鍵字
從上面我們可以看出c++對虛函數的要求比較嚴格,可能有的時候參數類型寫錯了導致無法構成重寫.我們就可以override來幫助我們進行檢查.
class D
{
public:virtual void d(){cout << "dadad" << endl;}
};
class E:public D
{
public:virtual void d(int a) override{cout << "dada" << endl;}
};
final關鍵字我們已經見過了,我們在實現一個不能被繼承的類的時候,我們用final修飾或者構造私有.
而在這里我們不想讓虛函數被繼承也可用final來進行修飾.
3.5重載/重寫/隱藏對比
重載
1.在統一作用域
2.函數名相同,參數不同,返回值可相同,可不同
重寫
1.在統一繼承體系下的不同的基類和派生類的作用域之中.
2.函數名,參數,返回值都必須相同,協變例外
3.兩個函數都必須時虛函數
隱藏
1.在統一繼承體系下的不同的基類和派生類的作用域之中.
2.函數名相同
3.兩個函數只要不是重寫就是隱藏.
4.變量名相同也可以構成隱藏
隱藏和重寫的二者上是有所重疊但是并不完全相同
3.6純虛函數與抽象類
在虛函數的后面加上=0,這個虛函數就是純虛函數,純虛函數所在的類被稱為抽象類,抽象類是不能夠實例化對象的,并且抽象類被繼承之后的派生類的虛函數一定要被重寫.
否則這個類也是抽象類.
class F
{
public:virtual void ff() = 0;};
class G :public F
{virtual void ff(){cout << "dada" << endl;}
};
4.多態的原理
class Base{public:virtual void Func1(){cout << "Func1()" << endl;}protected:int _b = 1;char _ch = 'x';};
當我們去算上面的類的時候,我們正常的結果是8bytes.
實際上則不同
它的大小是12bytes.
當我們創建一個Base類的對象來看的時候,我們發現除了上面的我們類中創建兩個成員變量還有一個vfptr的函數指針.在x86的環境下它的大小就是12bytes.
這個指針就是虛函數表指針,每一個含有虛函數的類中,至少含有一個虛函數表,這個表里面放在虛函數的地址
從底層的角度我們該如何看到a是如何被調用的呢,當父類指針ptr1指向A的時候調用A的a函數,ptr2指向b的時候調用b中的a函數呢.
實際上當調用虛函數的時候,去調用函數的地址的時候,不是編譯時通過對象來確定虛函數的地址.而是通過對象中的虛表來去call這個虛函數的地址
class Person {public:virtual void BuyTicket() { cout << "買票全價" << endl; }private:
string _name;};class Student : public Person {public:virtual void BuyTicket() { cout << "買票打折" << endl; }private:
string _id;};class Soldier: public Person {public:virtual void BuyTicket() { cout << "買票優先" << endl; }private:
string _codename;};void Func(Person* ptr){// 這?可以看到雖然都是Person指針Ptr在調?BuyTicket
// 但是跟ptr沒關系,?是由ptr指向的對象決定的。ptr->BuyTicket();}int main(){// 其次多態不僅僅發?在派?類對象之間,多個派?類繼承基類,重寫虛函數后// 多態也會發?在多個派?類之間。Person ps;Student st;Soldier sr;Func(&ps);Func(&st);Func(&sr);return 0;}
4.1動態綁定與靜態綁定
對于通過動態多態(父類的指針或者引用)去調用的函數,也就是運行時到指定對象的虛函數表中去調用函數的,我們叫做動態綁定.
對不滿足動態多態條件的在編譯時確定函數地址或者通過對象去確定函數的地址的,我們叫做靜態綁定
4.2虛函數表
1.基類的虛函數表中存放著所以基類虛函數的地址,同一類型的對象公用同一張虛表,不同類的虛表之間時獨立的.基類和派生類的虛表時相互獨立的
2.派?類由兩部分構成,繼承下來的基類和??的成員,?般情況下,繼承下來的基類中有虛函數表指針,??就不會再?成虛函數表指針。但是要注意的這?繼承下來的基類部分虛函數表指針和基類對象的虛函數表指針不是同?個,就像基類對象的成員和派?類對象中的基類對象成員也獨?的。
3.派?類中重寫的基類的虛函數,派?類的虛函數表中對應的虛函數就會被覆蓋成派?類重寫的虛函數地址。
4.派?類的虛函數表中包含,(1)基類的虛函數地址,(2)派?類重寫的虛函數地址完成覆蓋,派?類??的虛函數地址三個部分。
5.虛函數表本質是?個存虛函數指針的指針數組,?般情況這個數組最后?放了?個0x00000000標記。(這個C++并沒有進?規定,各個編譯器??定義的,vs系列編譯器會再后?放個0x00000000標記,g++系列編譯不會放)
6.虛函數存在哪的?虛函數和普通函數?樣的,編譯好后是?段指令,都是存在代碼段的,只是虛函數的地址?存到了虛表中。
7.虛函數表存在哪的?這個問題嚴格說并沒有標準答案C++標準并沒有規定,我們寫下?的代碼可以對?驗證?下。vs下是存在代碼段(常量區)
class Base {public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }void func5() { cout << "Base::func5" << endl; }protected:int a = 1;};class Derive : public Base{public:// 重寫基類的func1
virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func1" << endl; }
int main(){int i = 0;static int j = 1;int* p1 = new int;const char* p2 = "xxxxxxxx";printf("棧:%p\n", &i);printf("靜態區:%p\n", &j);printf("堆:%p\n", p1);printf("常量區:%p\n", p2);Base b;Derive d;Base* p3 = &b;Derive* p4 = &d;printf("Person虛表地址:%p\n", *(int*)p3);printf("Student虛表地址:%p\n", *(int*)p4);printf("虛函數地址:%p\n", &Base::func1);printf("普通函數地址:%p\n", &Base::func5);return 0;}