C++虛表&多態(tài)的實(shí)現(xiàn)原理分析
C++虛表&多態(tài)實(shí)現(xiàn)原理
這里我們只看繼承中的多態(tài) ,本文程序調(diào)試在VS2017
先看一個(gè)小問題,下面的類A實(shí)例化出的對(duì)象占幾個(gè)字節(jié)呢?
#include<iostream>
using namespace std;
class A {
int m_a;
public:
void func(){
cout << "類A的func" << endl;
}
};
int main() {
A a;
cout << sizeof(a) << endl;
system("pause");
return 0;
}
答案是4字節(jié),這是因?yàn)?strong>成員函數(shù)存放在公共的代碼段, 所以只計(jì)算成員變量m_a所占字節(jié)的大小
調(diào)試一下來看,

我們?nèi)绻麑⒊蓡T函數(shù)定義成虛函數(shù)又會(huì)如何呢?
我們來看:
#include<iostream>
using namespace std;
class A {
int m_a;
public:
virtual void func(){
cout << "類A的func" << endl;
}
virtual int func1() {
cout << "類A的func1" << endl;
return 0;
}
};
int main() {
A a,b;
cout << sizeof(A) << endl;
system("pause");
return 0;
}
可以看到結(jié)果是8字節(jié),emm.... 事出反常必有妖,我們調(diào)試一下,看看到底多出來了個(gè)什么東西

我們可以看到,在實(shí)例化出的對(duì)象a和b中多了一個(gè)_vfptr,它的類型時(shí)void**,是一個(gè)二級(jí)指針,指針在32位平臺(tái)中占4字節(jié),所以這里的結(jié)果是8(m_a的4字節(jié)+_vfptr的4字節(jié)),那么_vfptr到底是個(gè)什么東西?
類中有了虛函數(shù)之后才有了_vfptr,它們之間到底有著什么關(guān)系?
其實(shí),_vfptr,其實(shí)就是虛函數(shù)指針 (virtual function pointer)

可以看到_vfptr 指向了一個(gè) vftable(virtual function table) 虛函數(shù)表(也叫虛表),虛表中元素是void*類型 ,第一個(gè)元素是指向了虛函數(shù)func(),第二個(gè)元素指向了fun1()
虛函數(shù)指針(_vfptr) 和 虛函數(shù)表(vftable)
虛函數(shù)指針和虛表是什么
通過上面的調(diào)試,我們已經(jīng)看到了,_vfptr是指向虛表的一個(gè)指針,那么我們也可以叫 _vfptr 為虛函數(shù)表指針
- 當(dāng)一個(gè)類中有虛函數(shù)時(shí),編譯期間,就會(huì)為這個(gè)類分配一片連續(xù)的內(nèi)存 (這就是虛表vftable),來存放虛函數(shù)的地址,類中只保存著
- 指向虛表的指針 (也就是虛函數(shù)表指針_vfptr) ,(虛函數(shù)其實(shí)和普通函數(shù)一樣,存放在代碼段) ,當(dāng)這個(gè)類實(shí)例出對(duì)象時(shí),每個(gè)對(duì)象
- 都會(huì)有一個(gè)虛函數(shù)表指針_vfptr (VS中虛表內(nèi)存分配在代碼段)
虛表本質(zhì)上是一個(gè)在編譯時(shí)就已經(jīng)確定好了的void* 類型的指針數(shù)組 .
注意 : 虛函數(shù)表為了標(biāo)志結(jié)尾,會(huì)在虛表最后一個(gè)元素位置保存一個(gè)空指針.所以看到的虛表元素個(gè)數(shù)比實(shí)際虛函數(shù)個(gè)數(shù)多一個(gè)
C++中的虛函數(shù)的實(shí)現(xiàn)一般是通過虛函數(shù)表 (C++規(guī)范并沒有規(guī)定具體用哪種方法,但大部分的編譯器都用虛函數(shù)表的方法) 大多數(shù)編譯器(如本文用的VS)中虛函數(shù)表指針都在對(duì)象的最前面位置,意味著能通過對(duì)象的地址就能遍歷虛函數(shù)表(能夠在多層繼承或多重繼承中保持較高性能)
虛函數(shù)是為了繼承時(shí)的多態(tài)才有的概念,上面簡(jiǎn)單了解了一下虛表,我們?cè)賮砜蠢^承關(guān)系中的虛表
繼承中的虛表
在有虛函數(shù)的類(有虛表的類)被繼承后, 虛表也會(huì)被拷貝給派生類. 注意,編譯器會(huì)給派生類新分配一片空間來拷貝基類的虛表,將這個(gè)虛表的指針給派生類, 而并不是沿用基類的虛表,在發(fā)生虛函數(shù)的重寫時(shí),重寫的是派生類為了拷貝基類虛表新創(chuàng)建的這虛表中的虛函數(shù)地址
虛表為所有這個(gè)類的對(duì)象所共享. 注意,是通過給每個(gè)對(duì)象一個(gè)虛表指針_vfptr共享到的虛表.
單繼承中的虛表
1. 單繼承未重寫虛函數(shù): 會(huì)繼承派生類的虛表,如果派生類中新增了虛函數(shù),則會(huì)加繼承的虛表后面
2. 單繼承重寫虛函數(shù): 繼承的虛表中被重寫的虛函數(shù)地址會(huì)在繼承虛表時(shí)被修改為派生類函數(shù)的地址(如下面例子中把A::func()修改成了B::func()的地址)(注意: 此時(shí)基類的虛表并沒有被修改,修改的是派生類自己的虛表)
所以, 重寫實(shí)際上就是在繼承基類虛表時(shí),把基類的虛函數(shù)地址修改為派生類虛函數(shù)的地址
舉個(gè)栗子
#include<iostream>
using namespace std;
class A {
int m_a;
public:
virtual void func(){
cout << "類A的func" << endl;
}
virtual int func1() {
cout << "類A的func1" << endl;
return 0;
}
};
class B :public A {
public:
virtual void func() {
cout << "類B的func" << endl;
}
virtual void func2() {
cout << "類B的func2" << endl;
}
};
int main() {
A a1;
A a2;
B b;
system("pause");
return 0;
}調(diào)試如下:

可以看到對(duì)象b中的_vfptr所指向的虛表繼承了類A的虛表 ,但是地址卻和a1,a2的_vfptr不一樣,也印證了前面所說,新分配了一片空間來拷貝基類的虛表.
還可以看到a1和a2的虛表地址相同,也印證了前面所說,類的虛表被所有對(duì)象所共享.
但我們卻發(fā)現(xiàn),B中也有虛函數(shù),怎么沒有了,講道理這是不科學(xué)的,那么B類的虛函數(shù)的地址放到底哪去了呢?
思考一下,它肯定是存在的,要么另外建一張?zhí)摫矸爬锩?,要么放在繼承A的虛表里.
實(shí)際上,在單繼承中,派生類的虛函數(shù)地址會(huì)放在繼承來的基類的虛表后面,只是VS這里沒有顯示出來.
我們也可以看到圖中紅色圈出來的vftable[4],虛表中也已經(jīng)有了四個(gè)元素,既然VS不給力,只能自己想辦法,可以在監(jiān)視窗口,通過地址看到,B類中的虛函數(shù)指針指向的虛表,其實(shí)是這樣的,如下

還可以看到,繼承的A中的虛函數(shù)func()被重寫之后,虛表中就放的是重寫后的B中的func()的地址
多繼承中的虛表
- 多繼承不重寫虛函數(shù): 繼承的多個(gè)基類中有多張?zhí)摫?,派生類?huì)全部拷貝下來,成為派生類的多張?zhí)摫恚绻缮愑行碌奶摵瘮?shù),會(huì)加在派生類拷貝的第一張?zhí)摫淼暮竺?拷貝的第一張?zhí)摫硎抢^承的第一個(gè)有虛函數(shù)(或虛表)的基類的)
- 多繼承重寫虛函數(shù) : 規(guī)則與 不重寫虛函數(shù) 相同,但需要注意的是,如果多個(gè)基類中含有相同的虛函數(shù),例如func. 當(dāng)派生類重寫func這個(gè)虛函數(shù)后,所有含有這個(gè)函數(shù)的基類虛表都會(huì)被重寫 (改的是派生類自己拷貝的基類虛表,并不是基類自己的虛表)
舉個(gè)栗子
#include<iostream>
using namespace std;
class A {
int m_a;
public:
virtual void funcA() {
cout << "類A的funcA" << endl;
}
virtual void func() {
cout << "類A的func" << endl;
}
};
class B {
public:
virtual void funcB() {
cout << "類B的funcB" << endl;
}
virtual void func() {
cout << "類B的func" << endl;
}
};
class C :public A,public B {
public:
virtual void func() {
cout << "類C的func" << endl;
}
virtual void funcC() {
cout << "類C的funcC" << endl;
}
};
int main() {
C c;
A a;
system("pause");
return 0;
}調(diào)試如下 :

可以看到,派生類繼承了兩張?zhí)摫?,A::func()和B::func()的地址修改為了C:::func()的地址

我們?cè)俅瓮ㄟ^_vfptr的地址,在監(jiān)視窗口可以看到,派生類中新增的虛函數(shù),虛函數(shù)地址被加在了派生類拷貝基類的第一張?zhí)摫淼暮竺?
多態(tài)的原理
我們回憶一下多態(tài)的兩個(gè)構(gòu)成條件
- 1.通過指向派生類對(duì)象的基類的指針或引用調(diào)用虛函數(shù)
- 2. 被調(diào)用的函數(shù)必須是被派生類重寫過的虛函數(shù)
簡(jiǎn)單來說就是,利用了虛函數(shù)可以重寫的特性,當(dāng)一個(gè)有虛函數(shù)的基類有多個(gè)派生類時(shí),通過各個(gè)派生類對(duì)基類虛函數(shù)的不同重寫,實(shí)現(xiàn)通過指向派生類對(duì)象的基類指針或基類引用調(diào)用同一個(gè)虛函數(shù),去實(shí)現(xiàn)不同功能的特性. 抽象來說就是,為了完成某個(gè)行為,不同的對(duì)象去完成時(shí)會(huì)產(chǎn)生多種不同的狀態(tài)
總結(jié)
- 一個(gè)有虛函數(shù)的類,它實(shí)例出的所有對(duì)象通過虛表指針vfptr共享類的虛表
- 對(duì)象中存放的是虛函數(shù)(表)指針vfptr,不是虛表. vfptr是虛表的首地址,指向虛表
- 虛表中存放的時(shí)虛函數(shù)地址,不是虛函數(shù),虛函數(shù)和普通函數(shù)一樣,存放在代碼段
- 虛表是在編譯階段生成的,一般分配在在代碼段(常量區(qū)),例如VS中
派生類的虛表生成:
- a.先將基類中的虛表內(nèi)容拷貝一份到派生類虛表中
- b.如果派生類重寫了基類中某個(gè)虛函數(shù),用派生類自己的虛函數(shù)覆蓋虛表中基類的虛函數(shù)
- c.派生類自己新增加的虛函數(shù)按其在派生類中的聲明次序增加到派生類虛表的最后。
- d.如是多繼承,則派生類新增加的虛函數(shù)地址添加在派生類拷貝(繼承)的第一張?zhí)摫砗竺?/li>
以上為個(gè)人經(jīng)驗(yàn),希望能給大家一個(gè)參考,也希望大家多多支持腳本之家。
相關(guān)文章
C++實(shí)現(xiàn)LeetCode(155.最小棧)
這篇文章主要介紹了C++實(shí)現(xiàn)LeetCode(155.最小棧),本篇文章通過簡(jiǎn)要的案例,講解了該項(xiàng)技術(shù)的了解與使用,以下就是詳細(xì)內(nèi)容,需要的朋友可以參考下2021-07-07
C++實(shí)現(xiàn)簡(jiǎn)單的計(jì)算器功能
這篇文章主要為大家詳細(xì)介紹了C++實(shí)現(xiàn)簡(jiǎn)單的計(jì)算器功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-01-01
C++實(shí)現(xiàn)封裝的順序表的操作與實(shí)踐
在程序設(shè)計(jì)中,順序表是一種常見的線性數(shù)據(jù)結(jié)構(gòu),通常用于存儲(chǔ)具有固定順序的元素,與鏈表不同,順序表中的元素是連續(xù)存儲(chǔ)的,因此訪問速度較快,但插入和刪除操作的效率可能較低,本文將詳細(xì)介紹如何用 C++ 語言實(shí)現(xiàn)一個(gè)封裝的順序表類,深入探討順序表的核心操作2025-02-02
C++ 析構(gòu)函數(shù)與變量的生存周期實(shí)例詳解
這篇文章主要介紹了C++ 析構(gòu)函數(shù)與變量的生存周期實(shí)例詳解的相關(guān)資料2017-06-06

