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

1.3 多態(tài)的原理
多態(tài)是如何實(shí)現(xiàn)指向誰(shuí)就調(diào)用誰(shuí)的虛函數(shù)的?
在運(yùn)行時(shí),多態(tài)會(huì)到指向?qū)ο蟮奶摫碇胁檎乙{(diào)用的虛函數(shù)的地址,父類對(duì)象的虛表中的虛函數(shù)指針指向的是父類虛函數(shù),而子類對(duì)象的虛表中的虛函數(shù)指針指向的是子類重寫(xiě)后的虛函數(shù)。
class Person {
public:
virtual void BuyTicket() { cout << "買票-全價(jià)" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "買票-半價(jià)" << endl; }
};
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person Mike;
Func(Mike);
Student Johnson;
Func(Johnson);
return 0;
}
- 當(dāng) p 是指向mike對(duì)象時(shí),p->BuyTicket在mike的虛表中找到虛函數(shù)是Person::BuyTicket。
- 當(dāng) p 是指向johnson對(duì)象時(shí),p->BuyTicket在johson的虛表中找到虛函數(shù)是Student::BuyTicket。
也就是說(shuō),調(diào)用函數(shù)時(shí),他其實(shí)并不知道自己要調(diào)用的是子類還是父類的虛函數(shù),他只需要到這個(gè)對(duì)象的虛表里面找就行了,找到是哪個(gè)就是哪個(gè)。如果是父類對(duì)象,那就直接通過(guò)虛表指針到虛表里找。
如果是子類對(duì)象,我們知道:
派生類對(duì)象 可以賦值給 基類的對(duì)象 / 基類的指針 / 基類的引用。這里有個(gè)形象的說(shuō)法叫切片或者切割。寓意把派生類中父類那部分切來(lái)賦值過(guò)去。
子類對(duì)象中的父類部分會(huì)被切割賦給 p,p 還是按照父類對(duì)象找虛函數(shù)的方式去虛表里找。這樣就實(shí)現(xiàn)了不同對(duì)象去完成同一行為時(shí),展現(xiàn)出不同的形態(tài)。
PS:滿足多態(tài)以后的函數(shù)調(diào)用,不是在編譯時(shí)確定的,是運(yùn)行起來(lái)以后到指向?qū)ο蟮奶摵瘮?shù)表中查找對(duì)應(yīng)的虛函數(shù)的地址。不滿足多態(tài)的函數(shù)調(diào)用是編譯時(shí)直接確定的,通過(guò) p 的類型確定要調(diào)用函數(shù)的地址。
1.4 動(dòng)態(tài)綁定與靜態(tài)綁定
- 靜態(tài)綁定又稱為前期綁定(早綁定),在程序編譯期間確定了程序的行為,也稱為靜態(tài)多態(tài),比如:函數(shù)重載
- 動(dòng)態(tài)綁定又稱后期綁定(晚綁定),是在程序運(yùn)行期間,根據(jù)具體拿到的類型確定程序的具體行為,調(diào)用具體的函數(shù),也稱為動(dòng)態(tài)多態(tài)。
int i = 0; double d = 1.1; // 靜態(tài)綁定 靜態(tài)的多態(tài)(靜態(tài):編譯時(shí)確定函數(shù)) f1(i); f1(d); // 動(dòng)態(tài)綁定 動(dòng)態(tài)的多態(tài)(一般的多態(tài)指的就是動(dòng)態(tài)多態(tài))(動(dòng)態(tài):運(yùn)行時(shí)去虛表找函數(shù)) Base* p = new Base; p->Func1(); p = new Derive; p->Func1();
2. 單繼承和多繼承關(guān)系的虛函數(shù)表
需要注意的是在單繼承和多繼承關(guān)系中,下面我們?nèi)リP(guān)注的是派生類對(duì)象的虛表模型,因?yàn)榛惖奶摫砟P颓懊嫖覀円呀?jīng)看過(guò)了,沒(méi)什么需要特別研究的
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 對(duì)象的 func3 和 func4 呢?監(jiān)視窗口好像沒(méi)有,實(shí)際上不是虛表中沒(méi)有,而是監(jiān)視窗口沒(méi)有展示,它認(rèn)為不需要展示,就沒(méi)有顯示出來(lái)。
我們可以自己打印虛表,看看到底有沒(méi)有 func3 和 func4 :
typedef void(*VF_PTR)(); // 函數(shù)指針類型重定義
// 定義完成后可以使用 VF_PTR p;來(lái)創(chuàng)建一個(gè)函數(shù)指針對(duì)象
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ù)指針指向的這個(gè)函數(shù)
}
cout << endl;
}
int main()
{
Base b;
Derive d;
// 取對(duì)象中前四個(gè)字節(jié)存的虛表指針打印虛表
PrintVFTable((VF_PTR*)(*(int*)&b));
PrintVFTable((VF_PTR*)(*(int*)&d));
return 0;
}
事實(shí)證明 func3 和 func4 確實(shí)存在于虛表中,我們成功打印并且調(diào)用了。
2.2 多繼承中的虛函數(shù)表
typedef void(*VF_PTR)(); // 函數(shù)指針類型重定義
// 定義完成后可以使用 VF_PTR p;來(lái)創(chuàng)建一個(gè)函數(shù)指針對(duì)象
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ù)指針指向的這個(gè)函數(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;
}
- 這說(shuō)明子類 d 對(duì)象的虛函數(shù) func3 是往第一個(gè)繼承的父類 base1 的虛表里放的。
- 另外也說(shuō)明了先繼承的父類,它的虛表放在前面(即低地址處)。
2.3 菱形繼承、菱形虛擬繼承
實(shí)際中不建議設(shè)計(jì)出菱形繼承及菱形虛擬繼承,一方面太復(fù)雜容易出問(wèn)題,另一方面這樣的模型,訪問(wèn)基類成員有一定得性能損耗。
3. Q&A
3.1 內(nèi)聯(lián)函數(shù)為什么不能是虛函數(shù)?
內(nèi)聯(lián)函數(shù)會(huì)在調(diào)用位置直接展開(kāi),所以內(nèi)聯(lián)函數(shù)沒(méi)有地址,也不需要地址,沒(méi)有函數(shù)地址就無(wú)法放入虛表,所以內(nèi)聯(lián)函數(shù)不能是虛函數(shù)。
3.2 靜態(tài)函數(shù)為什么不能是虛函數(shù)?
1. 調(diào)用方式與對(duì)象綁定的根本差異
靜態(tài)函數(shù):
- 不依賴于任何類的實(shí)例(對(duì)象)
- 可以直接通過(guò)類名調(diào)用(
ClassName::StaticFunction()) - 沒(méi)有
this指針,無(wú)法訪問(wèn)對(duì)象的非靜態(tài)成員
虛函數(shù):
- 完全依賴于類的實(shí)例(對(duì)象)
- 必須通過(guò)對(duì)象或?qū)ο笾羔樥{(diào)用
- 有
this指針,可以訪問(wèn)對(duì)象的非靜態(tài)成員 - 通過(guò)虛函數(shù)表(vTable)實(shí)現(xiàn)動(dòng)態(tài)綁定,而vTable是每個(gè)對(duì)象實(shí)例的一部分
2. 虛函數(shù)機(jī)制依賴于對(duì)象實(shí)例
虛函數(shù)的實(shí)現(xiàn)依賴于:
- 每個(gè)對(duì)象內(nèi)部的虛函數(shù)表指針(vPtr)
- 通過(guò)vPtr在運(yùn)行時(shí)查找正確的函數(shù)實(shí)現(xiàn)
靜態(tài)函數(shù)沒(méi)有this指針,因此無(wú)法訪問(wèn)對(duì)象的vPtr,也就無(wú)法實(shí)現(xiàn)動(dòng)態(tài)綁定。如果靜態(tài)函數(shù)是虛的,編譯器無(wú)法知道應(yīng)該使用哪個(gè)類的虛函數(shù)表。
3.3 虛表?虛基表?虛基類?
- 虛表是虛函數(shù)表,存儲(chǔ)的是虛函數(shù)指針,是一個(gè)函數(shù)指針數(shù)組。
- 虛基表存儲(chǔ)的是偏移量,是解決菱形繼承的數(shù)據(jù)冗余和二義性問(wèn)題的。
- 虛基類是在繼承中給父類前面加 virtual 關(guān)鍵字,是為了解決菱形繼承的數(shù)據(jù)冗余和二義性問(wèn)題而存在的。
總結(jié)
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
C++實(shí)現(xiàn)雷霆戰(zhàn)機(jī)可視化小游戲
這篇文章主要為大家詳細(xì)介紹了C++實(shí)現(xiàn)雷霆戰(zhàn)機(jī)可視化小游戲,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-11-11
C語(yǔ)言算法金手指摩爾投票法手撕絕大多數(shù)問(wèn)題
這篇文章主要為大家介紹了C語(yǔ)言算法之金手指摩爾投票法手撕絕大多數(shù)問(wèn)題的示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助2022-02-02
C++ 進(jìn)程間通信IPC的實(shí)現(xiàn)示例
本文主要介紹了C++ 進(jìn)程間通信IPC的實(shí)現(xiàn)示例,包括管道、消息隊(duì)列、共享內(nèi)存等基礎(chǔ)與高級(jí)方法,具有一定的參考價(jià)值,感興趣的可以了解一下2025-06-06
數(shù)據(jù)結(jié)構(gòu)之?dāng)?shù)組翻轉(zhuǎn)的實(shí)現(xiàn)方法
這篇文章主要介紹了數(shù)據(jù)結(jié)構(gòu)之?dāng)?shù)組翻轉(zhuǎn)的實(shí)現(xiàn)方法的相關(guān)資料,這里用幾種實(shí)現(xiàn)方法來(lái)實(shí)現(xiàn)這樣的功能,需要的朋友可以參考下2017-10-10
通過(guò)代碼實(shí)例解析c++ vector常用方法
這篇文章主要介紹了通過(guò)代碼實(shí)例解析c++ vector常用方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-07-07
基于c++強(qiáng)制類型轉(zhuǎn)換的(總結(jié))詳解
本篇文章對(duì)C++中的強(qiáng)制類型轉(zhuǎn)換進(jìn)行了詳細(xì)的分析介紹。需要的朋友參考下2013-05-05

