文章目錄
- 🐵1. 什么是多態
- 🐶2. 構成多態的條件
- 🐩2.1 虛函數
- 🐩2.2 虛函數的重寫
- 🐩2.3 final 和 override關鍵字
- 🐩2.4 重載、重寫、重定義對比
- 🐱3. 虛函數表
- 🐯4. 多態的原理
- 🐎5. 多繼承的虛表關系
- 🦬6. 抽象類
🐵1. 什么是多態
當下網絡有個熱門詞匯叫“雙標”,意思就是用不同的標準來衡量人或事,這是一個貶義詞。而在編程世界中,這種“雙標”,我們稱之為多態,當然了這里的多態并不是貶義詞,而是一種技術實現。
比如說某種商城有會員機制,將用戶分為普通用戶、普通會員、尊貴會員等
那買同種東西的時候,不同的用戶等級會有著不同的價格,這就是一種多態行為
🐶2. 構成多態的條件
實現多態性的主要構成條件是使用虛函數和繼承:
- 必須通過基類的指針或引用調用虛函數
- 被調用的函數必須是虛函數,且派生類必須對虛函數進行重寫
🐩2.1 虛函數
只有類的成員函數才能被定義為虛函數,格式如下:
class A
{//函數前面加上virtual 表面該成員函數為虛函數virtual void func() {}
};
🐩2.2 虛函數的重寫
當派生類中有一個和基類完全相同的虛函數時,我們稱這為虛函數的重寫/覆蓋
重寫有三同,即:返回值類型、函數名、參數列表完全相同
class A
{
public://虛函數virtual void func() const{cout << "A->func()" << endl;}
};
class B :public A
{
public://虛函數重寫virtual void func() const{cout << "B->func()" << endl;}
};
//多態調用傳引用過去
void Print(const A& p)
{p.func();
}
int main()
{Print(A()); //A->func()Print(B()); //B->func()return 0;
}
在多態調用中,看的是指向的對象;而普通的函數調用,看的是當前的類型
虛函數的重寫,還需注意幾點:
-
虛函數父類必須加上
virtual
修飾,子類虛函數重寫前面可以不加virtual
,但在實際中,還是建議加上 -
對于虛函數的重寫,我們規定三同,但是有例外——協變
即基類與虛函數返回值類型不同,但是返回值類型必須是構成父子關系指針或者引用(同時是指針 或 同時是引用)
class A { public://虛函數virtual A* func() const{cout << "A->func()" << endl;return 0;} }; class B :public A { public://虛函數重寫 B和A是父子關系virtual B* func() const{cout << "B->func()" << endl;return 0;} }; void Print(const A& p) {p.func(); } int main() {Print(A());Print(B());return 0; }
-
析構函數的重寫,基類和派生類的析構函數名不同
class A { public://虛函數virtual ~A(){cout << "~A()" << endl;} }; class B :public A { public://虛函數重寫virtual ~B(){cout << "~B()" << endl;} }; int main() {A* a1 = new A;A* a2 = new B;delete a1;delete a2;return 0; }
輸出:
這里的原因是因為編譯器對析構函數的名字做了處理,編譯后名稱統一處理為
destructor
,那為什么要將析構函數統一處理稱destructor
呢?因為這里要讓他們構成重寫。如果不構成重寫,就好出現類似這樣的情況:class A { public:~A(){cout << "~A()" << endl;} }; class B :public A { public:~B(){delete ptr;cout << "~B()" << endl;} protected:int* ptr; }; int main() {A* a1 = new A;delete a1;a1 = new B;delete a1;return 0; }
輸出發現,我們這里
new
了一個B對象,但是每次都是調用A的析構函數,這顯然與我們的意愿不符,我們期望的是這個a1->destructor
形成的是多態調用,所以這樣統一處理之后,就可以讓他們構成重寫
🐩2.3 final 和 override關鍵字
如果不想讓這個虛函數被重寫,可加上final
關鍵字修飾
當然了,final
也可以修飾類,讓這個類不被繼承,一般用于最終的類
如果要檢查某個派生類是否重寫了基類的某個虛函數,可用override
關鍵字修飾,如果沒有重寫,則編譯報錯
🐩2.4 重載、重寫、重定義對比
🐱3. 虛函數表
class A
{
public:virtual void func(){cout << "func()" << endl;}
protected:int _a;
};
int main()
{cout << sizeof(A) << endl;
}
這段代碼如果不加上virtual
,則輸出的是4;但是加上virtual
之后,輸出的是16(64位下,指針是8字節,然后內存對齊)
這是因為有了虛函數,這個類里面會多一個虛函數表的指針,這些表里面存的是虛函數的地址
但如果將這個虛函數沒有被重寫,那么派生類的虛函數表還是指向基類的虛函數;如果重寫了,則指向重寫的虛函數。
所以多態調用的時候,不管我們傳的是基類和派生類,在內存里看到的都是父類;普通調用是在編譯的時候就確定了地址,而多態調用時,運行時會到指向對象的虛表找函數的地址
動態綁定與靜態綁定:
- 靜態綁定:在編譯時確定調用哪個函數或方法。這是在編譯器根據變量的靜態類型(聲明類型)來決定調用哪個函數
- 動態綁定:在運行時根據對象的實際類型來確定調用哪個函數或方法。這是通過虛函數(在基類中聲明為虛函數,子類進行重寫)實現的。動態綁定適用于通過基類指針或引用調用虛函數的情況,確保調用正確的派生類函數
在這里虛表的地址,是存儲在哪里的呢?我們通過這段代碼來驗證
class A
{
public:virtual void func(){cout << "A->func()" << endl;}virtual void Func(){cout << "A->Func()" << endl;}int _a;
};
class B :public A
{
public:virtual void func(){cout << "B->func()" << endl;}
};
void Print(A a)
{a.func();
}
int main()
{A aa;B bb;int a = 0;printf("棧:%p\n", &a);static int b = 0;printf("靜態區:%p\n", &b);int* p = new int;printf("堆:%p\n", p);const char* str = "hello";printf("常量區:%p\n", str);//前四個字節,一定是虛表的地址printf("虛表a:%p\n", *((int*)&aa));printf("虛表b:%p\n", *((int*)&bb));
}
輸出發現虛表的地址和常量區的地址隔的較近,所以我們可以得出結論:虛表的地址存儲在常量區
另外,我們在Vs的監視窗口只能查看3個虛函數的地址,但這不代表這,內存里面只有三個虛函數的地址,我們可通過這段代碼進行驗證:
class A
{
public:virtual void func1(){cout << "A->func1()" << endl;}virtual void func2(){cout << "A->func2()" << endl;}virtual void func3(){cout << "A->func3()" << endl;}
};
class B :public A
{virtual void func3(){cout << "B->func3()" << endl;}virtual void func4(){cout << "B->func4()" << endl;}
};
//函數指針命名
typedef void (*Func_Ptr)();
//打印函數指針數組
void PrintVFT(Func_Ptr table[])
{for (size_t i= 0; table[i]!=nullptr ; i++){printf("[%d]:%p->", i, table[i]);Func_Ptr f = table[i];f();}printf("\n");
}
int main()
{A a;B b;int vft1 = *((int*)&a);PrintVFT((Func_Ptr*)vft1);int vft2 = *((int*)&b);PrintVFT((Func_Ptr*)vft2);return 0;
}
🐯4. 多態的原理
有了虛表的概念,這我們就能理解,為什么構成多必須是通過基類的指針或引用調用虛函數。因為只有父類的虛表才能既能指向父類,又能指向子類。
那這里還有一個問題就是,為什么必須是指針或引用呢?
class A
{
public:virtual void func(){cout << "A->func()" << endl;}virtual void Func(){cout << "A->Func()" << endl;}int _a;
};
class B :public A
{
public:virtual void func(){cout << "B->func()" << endl;}
};
void Print(A a)
{a.func();
}
int main()
{A a;a._a = 1;B b;b._a = 10;a = b;A* pa = &b;A& ref = b;
}
這段代碼調試發現,子類賦值給父類,父類會進行切片,這里值會拷貝過去,但是虛表并不會拷貝;因為如果拷貝了虛表的話,這樣父類對象中的虛表指向的是父類還是子類就混淆了
🐎5. 多繼承的虛表關系
上面講的內容,包括舉得例子都是單繼承的,所以就不再贅述。這里我們看一下多繼承里面的虛表是怎樣的
class A
{
public:virtual void func1(){cout << "A->func1()" << endl;}virtual void func2(){cout << "A->func2()" << endl;}
protected:int _a;
};
class B
{
public:virtual void func1(){cout << "B->func1()" << endl;}virtual void func2(){cout << "B->func2()" << endl;}
protected:int _b;
};
class C :public A, public B
{
public:virtual void func1(){cout << "C->func1()" << endl;}virtual void funcC(){cout << "C->funcC()" << endl;}
protected:int _c;
};
typedef void (*Func_Ptr)();
//打印函數指針數組
void PrintVFT(Func_Ptr table[])
{for (size_t i= 0; table[i]!=nullptr ; i++){printf("[%d]:%p->", i, table[i]);Func_Ptr f = table[i];f();}printf("\n");
}
int main()
{C c;cout<<sizeof(c)<<endl;int vft1 = *((int*)&c);//int vft2 = *((int*)(char*)&c + sizeof(A));B* ptr = &c;int vft2 = *((int*)ptr);PrintVFT((Func_Ptr*)vft1);PrintVFT((Func_Ptr*)vft2);
}
通過驗證,我們可以發現,C類里面有兩張虛表,一張是A的,一張是B的。而C里面的虛函數funcC()
的虛表,是存放在第一張虛表里面
但是,我們這里發現,重寫的func1()
函數,明明是一樣的,但是地址卻不一樣,我們這段代碼轉到匯編代碼查看
int main()
{C c;A* ptr1 = &c;B* ptr2 = &c;ptr1->func1();ptr2->func1();return 0;
}
我們發現,ptr1
是直接調用找個func1()
,而ptr2
最終調用的地址和ptr1
是一樣的,但是在jump
的,寄存器減了一個8,這個減8正好是c
的地址。ptr1
不用修改是因為正好指向了c
的起始地址,內存不看類型,只看地址
菱形繼承這里就不講了,很混亂~
🦬6. 抽象類
虛函數后面加上=0
,則這個函數為純虛函數,包含了純虛函數的類,叫做抽象類。
抽象類不能實例化出對象,之后繼承的派生類也不能實例化對象,只能重寫虛函數,派生類才能實例化出對象。這里規定了派生類必須重新虛函數,所以抽象類也叫接口類。
class A
{
public:virtual void func() = 0;
};
class B :public A
{
public:virtual void func(){cout << "B->func()" << endl;}
};
class C :public A
{
public:virtual void func(){cout << "C->func()" << endl;}
};
void Func(A*a)
{a->func();
}
int main()
{Func(new B);Func(new C);return 0;
}
那么本期的分享就到這里咯,我們下期再見,如果還有下期的話。