日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程语言 > c/c++ >内容正文

c/c++

C++ 虚函数和虚表

發(fā)布時間:2023/12/13 c/c++ 36 豆豆
生活随笔 收集整理的這篇文章主要介紹了 C++ 虚函数和虚表 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

幾篇寫的不錯的文章,本文是整合了這幾篇文章,感謝這些大佬

https://www.jianshu.com/p/00dc0d939119

https://www.cnblogs.com/hushpa/p/5707475.html

https://www.jianshu.com/p/91227e99dfd7

多態(tài):

多態(tài)是面相對象語言一個重要的特性,多態(tài)即讓同一個用戶自定義類型的對象在不同的決策時機呈現(xiàn)不同的行為實現(xiàn)
C++中的多態(tài)就分為

  • 編譯時多態(tài):就包括類成員函數(shù)重寫operator函數(shù)重載
  • 運行時多態(tài):C++編譯器在運行時,根據(jù)決策邏輯判斷傳入所對象的類型,然后查找并根據(jù)該類虛表中的虛成員函數(shù)的地址,以進行動態(tài)調(diào)度目標(biāo)類中的成員函數(shù)。

接下來就說下運行時多態(tài)的核心,虛函數(shù)和其背后的虛表。

虛函數(shù)

用virtual關(guān)鍵字修飾的函數(shù)就叫虛函數(shù)

因為vTable(虛表)是C++利用runtime來實現(xiàn)多態(tài)的工具,所以我們需要借助virtual關(guān)鍵字將函數(shù)代碼地址存入vTable來躲開靜態(tài)編譯期。這里我們先不深入探究,后面我會細(xì)說。

首先我們先來看一個沒有虛函數(shù),即沒有用到vTable的例子:

#include <iostream> #include <ctime> using std::cout; using std::endl;struct Animal { void makeSound() { cout << "動物叫了" << endl; } };struct Cow : public Animal { void makeSound() { cout << "牛叫了" << endl; } }; struct Pig : public Animal { void makeSound() { cout << "豬叫了" << endl; } }; struct Donkey : public Animal { void makeSound() { cout << "驢叫了" << endl; } };int main(int argc, const char * argv[]) {srand((unsigned)time(0));int count = 4;while (count --) {Animal *animal = nullptr;switch (rand() % 3) {case 0:animal = new Cow;break;case 1:animal = new Pig;break;case 2:animal = new Donkey;break;}animal->makeSound();delete animal;}return 0; }

程序中有一個基類Animal,它有一個makeSound()函數(shù)。有三個繼承自Animal的子類,分別是牛、豬、驢,并且實現(xiàn)了自己的makeSound()方法。很簡單的代碼,是吧。

我們運行程序,你覺得輸出結(jié)果會是什么呢?不錯,這里會連續(xù)執(zhí)行4次Animal的makeSound()方法,結(jié)果如下:

為什么?因為我們的基類Animal的makeSound()方法沒有使用Virtual修飾,所以在靜態(tài)編譯時就makeSound()的實現(xiàn)就定死了。調(diào)用makeSound()方法時,編譯器發(fā)現(xiàn)這是Animal指針,就會直接jump到makeSound()的代碼段地址進行調(diào)用。

ok,那么我們把Animal的makeSound()改為虛函數(shù),如下:

struct Animal { virtual void makeSound() { cout << "動物叫了" << endl; } };

運行會是怎樣?如你所料,多態(tài)已經(jīng)成功實現(xiàn):

?

接下來就是大家最關(guān)心的部分,這是怎么回事?編譯器到底做了什么?

虛表

為了說明方便,我們需要修改一下基類Animal的代碼,不改變其他子類,修改如下:

struct Animal {virtual void makeSound() { cout << "動物叫了" << endl; }virtual void walk() {}void sleep() {} };struct Cow : public Animal { void makeSound() { cout << "牛叫了" << endl; } }; struct Pig : public Animal { void makeSound() { cout << "豬叫了" << endl; } }; struct Donkey : public Animal { void makeSound() { cout << "驢叫了" << endl; } };

首先我們需要知道幾個關(guān)鍵點:

  • 函數(shù)只要有virtual,我們就需要把它添加進vTable。
  • 每個類(而不是類實例)都有自己的虛表,因此vTable就變成了vTables。
  • 虛表存放的位置一般存放在模塊的常量段中,從始至終都只有一份。詳情可在此參考
  • 我們怎么理解?從本例來看,我們的Animal、Cow、Pig、Donkey類都有自己的虛表,并且虛表里都有兩個地址指針指向makeSound()和walk()的函數(shù)地址。一個指針4個字節(jié),因此每個vTable的大小都是8個字節(jié)。如圖:

    ?

    他們的虛表中記錄著不同函數(shù)的地址值。可以看到Cow、Pig、Donkey重寫了makeSound()函數(shù)但是沒有重寫walk()函數(shù)。因此在調(diào)用makeSound()時,就會直接jump到自己實現(xiàn)的code Address。而調(diào)用walk()時,則會jump到Animal父類walk的Code Address。

    虛指針

    現(xiàn)在我們已經(jīng)知道虛表的數(shù)據(jù)結(jié)構(gòu)了,那么我們在堆里實例化類對象時是怎么樣調(diào)用到相應(yīng)的函數(shù)的呢?這就要借助到虛指針了(vPointer)。

    虛指針是類實例對象指向虛表的指針,存在于對象頭部,大小為4個字節(jié),比如我們的Donkey類的實例化對象數(shù)據(jù)結(jié)構(gòu)就如下:

    ?

    我們修改main函數(shù)里的代碼,如下:

    int main(int argc, const char * argv[]) {int count = 2;while (count --) {Animal *animal = new Donkey;animal->makeSound();delete animal;}return 0; }

    我們在堆中生成了兩個Donkey實例,運行結(jié)果如下:

    驢叫了 驢叫了 Program ended with exit code: 0

    ?

    沒問題。然后我們再來看看堆里的結(jié)構(gòu),就變成了這樣:

    ?進一步探究虛表的內(nèi)存布局

    #include <iostream> class Employee{ public:bool iService=true;virtual ~Employee(){};virtual void add_salary(){std::cout<<"add_salary method in Employee"<<std::endl;} };class Teamer:public Employee{ public:int idNo=1000;virtual ~Teamer(){}void add_salary(){std::cout<<"add_salary method in Teamer"<<std::endl;}virtual void info(){std::cout<<"Teamer info for Teamer"<<std::endl;}void show(){std::cout<<"show method in Teamer"<<std::endl;} }; int main(void){Employee *tm1=new Teamer();Employee *tm2=new Teamer();Employee *pp1=new Employee();Employee *pp2=new Employee();delete tm1,tm2,pp1,pp2; }

    在這里我們可以嘗試打印*tm1,*tm2,*pp1和 *pp2,如下圖所示

    從上圖的輸出中,我們要引入一個虛指針(_vptr)的概念

    • 虛類的對象初始化時會自動創(chuàng)建一個隱藏的數(shù)據(jù)成員_vptr指針指向虛表,此前聲明該虛類的對象編譯器也創(chuàng)建了該虛類的虛表
    • 后續(xù)同一個虛類所有對象實例共享同一個虛表,截圖中的tm1和tm2的隱藏指針指向同一個地址0x400cf0,pp1和pp2的虛表是同理如是.
    • 虛表表當(dāng)前的地址是一個已經(jīng)+16字節(jié)偏移后的內(nèi)存地址

    另外我們還打印出所有Teamer對象和Employee對象,他們獲得內(nèi)存分配都為16個字節(jié)。因此我們不妨在查看我們剛才實例化的所有對象。

    查看對象的內(nèi)存數(shù)據(jù)

    現(xiàn)在我們不妨看看剛才實例化的各個對象的內(nèi)存布局,使用x命令,因為每個對象的堆內(nèi)存塊尺寸都為16個字節(jié),因此我們使用x/16xb將他們的內(nèi)存數(shù)據(jù)轉(zhuǎn)存到屏幕中,如下圖所示。

    • _vptr在虛類的對象中就占用8個字節(jié),該_vptr存儲了指向該虛類的虛表的內(nèi)存地址值。
    • iService是一個bool類型僅占用1個字節(jié),另外高位的3個字節(jié)空間由于內(nèi)存對齊的原因都以0填充。
    • idNo是一個4字節(jié)的int類型,對于Teamer的對象0x03e8的值就是十進制的1000,對于Employee的對象這里的4個字節(jié)由于按8字節(jié)內(nèi)存對齊,僅作為填充位之用。

      ?

    備注:這里我們回顧了內(nèi)存對齊的相關(guān)知識。

    探究虛表的內(nèi)存布局

    我們從前文打印的第一個Teamer對象 tm1的信息中,可以知道其_vptr指針指向0x400cf0,你是否發(fā)現(xiàn)“<虛表 for Teamer+16>”的字樣。這個其實表明0x400cf0是已經(jīng)+16字節(jié)偏移后的地址值

    我們已經(jīng)在前文提到在首個的新的虛類對象且初始化時,編譯器會該類動態(tài)創(chuàng)建一個虛表,但為什么每個不同虛類的虛表都要額外偏移16個字節(jié)呢? 在本示例中,我們不妨減去這個偏移量,也即得到0x400ce0這個地址,然后使用x命令,該命令將300字節(jié)的內(nèi)存數(shù)據(jù)轉(zhuǎn)儲到屏幕。

    (gdb) x/300xb 0x400ce0

    上面的命令以十六進制格式打印300字節(jié),從0x400d00開始。 為什么要這個地址? 因為在上面我們看到類Teamer的虛表指針指向0x400d10,該地址已經(jīng)偏移0x10個字節(jié),即減去0x10就能得到原本虛表的地址。

    下圖中_ZTV是虛表的前綴,_ZTS是type-string(名稱)的前綴,_ZTI是type-info的前綴。

    我們從下圖可以得到很多虛表的內(nèi)存細(xì)節(jié)。

    • 每個Teamer虛表存在一個虛表表頭占用16個字節(jié),前8個字節(jié)0填充,后8個字節(jié)包含一個指向與該類對應(yīng)的typeinfo表的地址(沒必要理會,只需知道他們占用16個字節(jié)即可)。
    • 每個typeinfo表的前面也包含一個typeinfo name的信息(沒必要理會,l羅列出來只是讓你知道有這么一個描述字段)
    • 綠色的部分就是不同虛類的虛表,虛表就是包含了該類定義的所有virtual成員函數(shù)的函數(shù)地址。

      ?

    我們可以從上圖中綠色部分的內(nèi)存數(shù)據(jù)中即每行冒號之后的8字節(jié)空間提取有用的數(shù)據(jù),例如

    • 0x400cf0到0x400d08的內(nèi)存區(qū)域中的內(nèi)存數(shù)據(jù),對應(yīng)的是Teamer類類虛表中virtual成員函數(shù)地址的條目。
    • 0x400d30到0x400d40的內(nèi)存區(qū)域中的內(nèi)存數(shù)據(jù),對應(yīng)的是Employee類虛表中virtual成員函數(shù)地址的條目

    我們這兩個內(nèi)存區(qū)域的數(shù)據(jù)分別整理成如下表,注意寫本文時使用的是CentOS 7的x64小端機器,因此讀取圖中的內(nèi)存數(shù)據(jù)時,是從右向左讀取,因此整理下表每個內(nèi)存位置對應(yīng)的值,并且分別是有info symbol命令 再次查看每個內(nèi)存位置的值對應(yīng)的具體含義。

    結(jié)合整理如下表可知:虛表中的地址值分別代表虛擬類中對應(yīng)虛函數(shù)的地址

    虛表內(nèi)存布局

    ?

    更簡單獲取虛類的虛表條目的另外一條命令就是info vtbl,這里就不展示了,我們看到上圖的虛表中的虛解構(gòu)函數(shù)都成對地出現(xiàn),我們先暫不討論為什么會這樣,因為我日后會令起一文再闡述該問題。

    • 第一個解構(gòu)函數(shù),稱為完整對象解構(gòu)函數(shù)(complete object destructor),執(zhí)行銷毀操作時無需在對象上調(diào)用delete()。
    • 第二個解構(gòu)函數(shù)稱為刪除析構(gòu)函數(shù)( deleting destructor),在銷毀對象后調(diào)用delete()。
    • 兩者都摧毀了任何虛擬基類.一個獨立的非虛函數(shù)稱為基類對象解構(gòu)函數(shù)(base object destructor)執(zhí)行對象的銷毀操作,但不執(zhí)行其虛擬基類子對象的銷毀操作,并且不調(diào)用delete()。
    • 非虛函數(shù)是靜態(tài)綁定的(編譯時綁定),因此在虛表中不存在任何非虛函數(shù)。

    虛表構(gòu)建細(xì)節(jié)

    我們?nèi)匀皇褂蒙衔牡恼{(diào)用示例代碼

    int main(void){//Employee *tm1=new Teamer();Employee *tm2=new Teamer();Employee *pp1=new Employee();Employee *pp2=new Employee();delete tm1,tm2,pp1,pp2; }

    從上面的示例代碼中我們已經(jīng)知道

    ?

    • 首先,每個使用虛函數(shù)的類或從基類派生的虛函數(shù)的類都被賦予自己的虛表。該表只是C++編譯器在“編譯時”設(shè)置的靜態(tài)數(shù)組。虛表包含當(dāng)前類中所有虛成員函數(shù)的函數(shù)指針的相關(guān)條目,那么填入虛表的虛成員函數(shù)指針有四種來源。
    • 派生類本身原創(chuàng)定義的虛函數(shù),例如上圖的Teamer::info()函數(shù)。

    • 從父類繼承的虛成員函數(shù),且該函數(shù)未被派生類重寫

    • 從父類繼承的虛成員函數(shù),但該函數(shù)已被派生類重寫。值的注意的是,虛表的虛成員函數(shù)指針始終指向該類中的最新的派生版本的虛成員函數(shù)。理解這句話非常重要!舉個例子Teamer類從Employee類繼承了add_salary()函數(shù),但Teamer類重寫(注意:不是重載)了該add_salary()函數(shù),對于Teamer虛表來說,填入表中的add_salary()函數(shù)的地址是0x400b3e,而不是父類的add_salary()的地址0x400ab4。

    • 若當(dāng)前類定義了虛解構(gòu)函數(shù),那么該類的虛解構(gòu)函數(shù)的解構(gòu)函數(shù)的地址會“成雙成對”地填入虛表中。按照慣例,由于定義類時優(yōu)先定義解構(gòu)函數(shù),再實現(xiàn)其他成員函數(shù),因此該虛解構(gòu)函數(shù)對的地址通常會出現(xiàn)在表中頭兩行,上圖是很好的例證。

    • 然后,當(dāng)類對象實例化時會將*_vptr設(shè)置為指向該類的虛表。例如,當(dāng)創(chuàng)建類型為Teamer的對象時*_vptr設(shè)置為指向Teamer的虛表。構(gòu)造類型為Employee對象時,*_vptr設(shè)置為指向的Employee的虛表。我們這里先不討論virtual解構(gòu)函數(shù),目前只針對其他虛函數(shù)進行討論。
    • 對于基類Employee類型的對象,它只能訪問Employee的成員,Employee類型的對象無法訪問Teamer類的的成員函數(shù),因為地址為0x400ab4的地址僅指向Employee::salary()
    • 同理,Teamer類型的對象也只能訪問Teamer::add_salary()和Teamer::info()。

    總結(jié):

    用一張圖說明一切

    ?

    總結(jié)

    以上是生活随笔為你收集整理的C++ 虚函数和虚表的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。