C++之虛函數(shù)與多態(tài)的實現(xiàn)原理分析
1.多態(tài)的原理
1.1 虛函數(shù)表(簡稱虛表)
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};問:sizeof(Base) 是多少?
- 32位系統(tǒng)下,他是 8。
int main()
{
Base a;
cout << sizeof(a) << endl; // 8
return 0;
}
觀察監(jiān)視窗口,我們發(fā)現(xiàn)除了_b成員,還多一個__vfptr放在對象的前面(注意有些平臺可能會放到對象的最后面,這個跟平臺有關(guān)),對象中的這個指針我們叫做虛函數(shù)表指針(一般是__vftptr,即virtual function table ptr)。
一個含有虛函數(shù)的類中都至少都有一個虛函數(shù)表指針,因為虛函數(shù)的地址要被放到虛函數(shù)表中,虛函數(shù)表也簡稱虛表。
針對上面的代碼我們做出以下改造:
- 我們增加一個派生類Derive去繼承Base
- Derive中重寫Func1
- Base再增加一個虛函數(shù)Func2和一個普通函數(shù)Func3
class Base
{
public:
virtual void Func1(){cout << "Base::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;
}
觀察監(jiān)控窗口的信息,我們可以發(fā)現(xiàn):
- 派生類對象d中也有一個虛表指針,d對象由兩部分構(gòu)成,一部分是父類繼承下來的成員(Base 部分),一類是自己的成員(_d)。虛表指針就是圖中的__vfptr。
- 基類b對象和派生類d對象虛表是不一樣的,Func1 在 d 類中完成了重寫,所以d的虛表中存的是重寫的Derive::Func1,通過對比可以發(fā)現(xiàn),d對象中的Func1的虛表指針和b對象中的Func1虛表指針指向地址不一樣。所以虛函數(shù)的重寫也叫作覆蓋,覆蓋就是指虛表中虛函數(shù)的覆蓋。重寫是語法的叫法,覆蓋是原理層的叫法。
- Func2繼承下來后是虛函數(shù),所以放進(jìn)了虛表,但是因為Func2沒有重寫覆蓋,所以b對象中的Func2的虛表地址和d對象的Func2虛表地址一致。父類中的Func3也繼承下來了,但是不是虛函數(shù),所以不會放進(jìn)虛表。
1.2 虛函數(shù)表本質(zhì)
虛函數(shù)表本質(zhì)是一個存虛函數(shù)指針的指針數(shù)組,一般情況這個數(shù)組最后面放了一個 nullptr 標(biāo)志數(shù)組結(jié)束。
派生類的虛表生成:
- 先將基類中的虛表內(nèi)容拷貝一份到派生類虛表中。
- 如果派生類重寫了基類中某個虛函數(shù),用派生類自己的虛函數(shù)覆蓋虛表中基類的虛函數(shù)。
- 派生類自己新增加的虛函數(shù)按其在派生類中的聲明次序增加到派生類虛表的最后。
- 注意:對象中存的是虛表指針,不是虛表本身。虛表中存的是虛函數(shù)指針,不是虛函數(shù),虛函數(shù)和普通函數(shù)一樣的,都是存在代碼段的,vs下虛表本身是存在代碼段(常量區(qū))的。
- 同類型的對象共用一個虛表。

1.3 多態(tài)的原理
多態(tài)是如何實現(xiàn)指向誰就調(diào)用誰的虛函數(shù)的?
在運(yùn)行時,多態(tài)會到指向?qū)ο蟮奶摫碇胁檎乙{(diào)用的虛函數(shù)的地址,父類對象的虛表中的虛函數(shù)指針指向的是父類虛函數(shù),而子類對象的虛表中的虛函數(shù)指針指向的是子類重寫后的虛函數(shù)。
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);
return 0;
}
- 當(dāng) p 是指向mike對象時,p->BuyTicket在mike的虛表中找到虛函數(shù)是Person::BuyTicket。
- 當(dāng) p 是指向johnson對象時,p->BuyTicket在johson的虛表中找到虛函數(shù)是Student::BuyTicket。
也就是說,調(diào)用函數(shù)時,他其實并不知道自己要調(diào)用的是子類還是父類的虛函數(shù),他只需要到這個對象的虛表里面找就行了,找到是哪個就是哪個。如果是父類對象,那就直接通過虛表指針到虛表里找。
如果是子類對象,我們知道:
派生類對象 可以賦值給 基類的對象 / 基類的指針 / 基類的引用。這里有個形象的說法叫切片或者切割。寓意把派生類中父類那部分切來賦值過去。
子類對象中的父類部分會被切割賦給 p,p 還是按照父類對象找虛函數(shù)的方式去虛表里找。這樣就實現(xiàn)了不同對象去完成同一行為時,展現(xiàn)出不同的形態(tài)。
PS:滿足多態(tài)以后的函數(shù)調(diào)用,不是在編譯時確定的,是運(yùn)行起來以后到指向?qū)ο蟮奶摵瘮?shù)表中查找對應(yīng)的虛函數(shù)的地址。不滿足多態(tài)的函數(shù)調(diào)用是編譯時直接確定的,通過 p 的類型確定要調(diào)用函數(shù)的地址。
1.4 動態(tài)綁定與靜態(tài)綁定
- 靜態(tài)綁定又稱為前期綁定(早綁定),在程序編譯期間確定了程序的行為,也稱為靜態(tài)多態(tài),比如:函數(shù)重載
- 動態(tài)綁定又稱后期綁定(晚綁定),是在程序運(yùn)行期間,根據(jù)具體拿到的類型確定程序的具體行為,調(diào)用具體的函數(shù),也稱為動態(tài)多態(tài)。
int i = 0; double d = 1.1; // 靜態(tài)綁定 靜態(tài)的多態(tài)(靜態(tài):編譯時確定函數(shù)) f1(i); f1(d); // 動態(tài)綁定 動態(tài)的多態(tài)(一般的多態(tài)指的就是動態(tài)多態(tài))(動態(tài):運(yùn)行時去虛表找函數(shù)) Base* p = new Base; p->Func1(); p = new Derive; p->Func1();
2. 單繼承和多繼承關(guān)系的虛函數(shù)表
需要注意的是在單繼承和多繼承關(guān)系中,下面我們?nèi)リP(guān)注的是派生類對象的虛表模型,因為基類的虛表模型前面我們已經(jīng)看過了,沒什么需要特別研究的
2.1 單繼承中的虛函數(shù)表
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;
};
int main()
{
Base b;
Derive d;
return 0;
}
從監(jiān)視窗口我們可以看到虛表的內(nèi)容,但是我們 d 對象的 func3 和 func4 呢?監(jiān)視窗口好像沒有,實際上不是虛表中沒有,而是監(jiān)視窗口沒有展示,它認(rèn)為不需要展示,就沒有顯示出來。
我們可以自己打印虛表,看看到底有沒有 func3 和 func4 :
typedef void(*VF_PTR)(); // 函數(shù)指針類型重定義
// 定義完成后可以使用 VF_PTR p;來創(chuàng)建一個函數(shù)指針對象
void PrintVFTable(VF_PTR* pTable)
{
for (size_t i = 0; pTable[i] != 0; ++i)
{
printf("vfTable[%d]:%p->", i, pTable[i]);
VF_PTR f = pTable[i];
f(); // 調(diào)用函數(shù)指針指向的這個函數(shù)
}
cout << endl;
}
int main()
{
Base b;
Derive d;
// 取對象中前四個字節(jié)存的虛表指針打印虛表
PrintVFTable((VF_PTR*)(*(int*)&b));
PrintVFTable((VF_PTR*)(*(int*)&d));
return 0;
}
事實證明 func3 和 func4 確實存在于虛表中,我們成功打印并且調(diào)用了。
2.2 多繼承中的虛函數(shù)表
typedef void(*VF_PTR)(); // 函數(shù)指針類型重定義
// 定義完成后可以使用 VF_PTR p;來創(chuàng)建一個函數(shù)指針對象
void PrintVFTable(VF_PTR pTable[])
{
for (size_t i = 0; pTable[i] != 0; ++i)
{
printf("vfTable[%d]:%p->", i, pTable[i]);
VF_PTR f = pTable[i];
f(); // 調(diào)用函數(shù)指針指向的這個函數(shù)
}
cout << endl;
}
class Base1 {
public:
virtual void func1() { cout << "Base1::func1" << endl; }
virtual void func2() { cout << "Base1::func2" << endl; }
private:
int b1;
};
class Base2 {
public:
virtual void func1() { cout << "Base2::func1" << endl; }
virtual void func2() { cout << "Base2::func2" << endl; }
private:
int b2;
};
class Derive : public Base1, public Base2 {
public:
virtual void func1() { cout << "Derive::func1" << endl; }
virtual void func3() { cout << "Derive::func3" << endl; }
private:
int d1;
};
int main()
{
// base1虛表4 + int4 + base2虛表4 + int4 + int4 = 20字節(jié)
cout << sizeof(Derive) << endl; // 20
Derive d;
// base1 的虛表
PrintVFTable((VF_PTR*)(*(int*)&d));
// base2 的虛表
PrintVFTable((VF_PTR*)(*(int*)((char*)&d + sizeof(Base1))));
return 0;
}
- 這說明子類 d 對象的虛函數(shù) func3 是往第一個繼承的父類 base1 的虛表里放的。
- 另外也說明了先繼承的父類,它的虛表放在前面(即低地址處)。
2.3 菱形繼承、菱形虛擬繼承
實際中不建議設(shè)計出菱形繼承及菱形虛擬繼承,一方面太復(fù)雜容易出問題,另一方面這樣的模型,訪問基類成員有一定得性能損耗。
3. Q&A
3.1 內(nèi)聯(lián)函數(shù)為什么不能是虛函數(shù)?
內(nèi)聯(lián)函數(shù)會在調(diào)用位置直接展開,所以內(nèi)聯(lián)函數(shù)沒有地址,也不需要地址,沒有函數(shù)地址就無法放入虛表,所以內(nèi)聯(lián)函數(shù)不能是虛函數(shù)。
3.2 靜態(tài)函數(shù)為什么不能是虛函數(shù)?
1. 調(diào)用方式與對象綁定的根本差異
靜態(tài)函數(shù):
- 不依賴于任何類的實例(對象)
- 可以直接通過類名調(diào)用(
ClassName::StaticFunction()) - 沒有
this指針,無法訪問對象的非靜態(tài)成員
虛函數(shù):
- 完全依賴于類的實例(對象)
- 必須通過對象或?qū)ο笾羔樥{(diào)用
- 有
this指針,可以訪問對象的非靜態(tài)成員 - 通過虛函數(shù)表(vTable)實現(xiàn)動態(tài)綁定,而vTable是每個對象實例的一部分
2. 虛函數(shù)機(jī)制依賴于對象實例
虛函數(shù)的實現(xiàn)依賴于:
- 每個對象內(nèi)部的虛函數(shù)表指針(vPtr)
- 通過vPtr在運(yùn)行時查找正確的函數(shù)實現(xiàn)
靜態(tài)函數(shù)沒有this指針,因此無法訪問對象的vPtr,也就無法實現(xiàn)動態(tài)綁定。如果靜態(tài)函數(shù)是虛的,編譯器無法知道應(yīng)該使用哪個類的虛函數(shù)表。
3.3 虛表?虛基表?虛基類?
- 虛表是虛函數(shù)表,存儲的是虛函數(shù)指針,是一個函數(shù)指針數(shù)組。
- 虛基表存儲的是偏移量,是解決菱形繼承的數(shù)據(jù)冗余和二義性問題的。
- 虛基類是在繼承中給父類前面加 virtual 關(guān)鍵字,是為了解決菱形繼承的數(shù)據(jù)冗余和二義性問題而存在的。
總結(jié)
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
C語言中變量與其內(nèi)存地址對應(yīng)的入門知識簡單講解
這篇文章主要介紹了C語言中變量與其內(nèi)存地址對應(yīng)的入門知識簡單講解,同時這也是掌握指針部分知識的基礎(chǔ),需要的朋友可以參考下2015-12-12
Visual Studio 2019下配置 CUDA 10.1 + TensorFlow-GPU 1.14.0
這篇文章主要介紹了Visual Studio 2019下配置 CUDA 10.1 + TensorFlow-GPU 1.14.0,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-03-03
C語言實現(xiàn)BMP格式圖片轉(zhuǎn)化為灰度
這篇文章主要為大家詳細(xì)介紹了C語言實現(xiàn)BMP格式圖片轉(zhuǎn)化為灰度,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-10-10
C語言數(shù)據(jù)結(jié)構(gòu)之單向鏈表詳解
單向鏈表(單鏈表)是鏈表的一種,其特點是鏈表的鏈接方向是單向的,對鏈表的訪問要通過順序讀取從頭部開始。本文將為大家詳細(xì)講講單向鏈表的實現(xiàn)與使用,需要的可以參考一下2022-08-08

