亚洲欧美第一页_禁久久精品乱码_粉嫩av一区二区三区免费野_久草精品视频

蟲蟲首頁| 資源下載| 資源專輯| 精品軟件
登錄| 注冊

您現在的位置是:首頁 > 技術閱讀 >  C++為什么要弄出虛表這個東西?

C++為什么要弄出虛表這個東西?

時間:2024-02-10

首先聲明一點,虛表并非是C++語言的官方標準的一部分,只是各家編譯器廠商在實現多態時的解決方案。另外即使同為虛表不同的編譯器對于虛表的設計可能也是不同的,本文主要基于Itanium C++ ABI(適用于gcc和clang)。

從C的POD類型到C++的類

首先回顧一下C語言純POD的結構體(struct)。如果用C語言實現一個類似面向對象的類,應該怎么做呢?

寫法一

#include <stdio.h>
typedef struct Actress {
    int height; // 身高
    int weight; // 體重
    int age;    // 年齡(注意,這不是數據庫,不必一定存儲生日)

    void (*desc)(struct Actress*);
} Actress;

// obj中各個字段的值不一定被初始化過,
// 通常還會在類內定義一個類似構造函數的函數指針,這里簡化
void profile(Actress* obj) {
    printf("height:%d weight:%d age:%d\n", obj->height, obj->weight, obj->age);
}

int main() {
    Actress a;
    a.height = 168;
    a.weight = 50;
    a.age = 20;
    a.desc = profile;

    a.desc(&a);
    return 0;
}

想達到面向對象中數據和操作封裝到一起的效果,只能給struct里面添加函數指針,然后給函數指針賦值。然而在C語言的項目中你很少會看到這種寫法,主要原因就是函數指針是有空間成本的,這樣寫的話每個實例化的對象中都會有一個指針大?。ū热?字節)的空間占用,如果實例化N個對象,每個對象有M個成員函數,那么就要占用N*M*8的內存。

所以通常C語言不會用在struct內定義成員函數指針的方式,而是直接:

寫法二

#include <stdio.h>
typedef struct Actress {
    int height; // 身高
    int weight; // 體重
    int age;    // 年齡(注意,這不是數據庫,不必一定存儲生日)

} Actress;

void desc(Actress* obj) {
    printf("height:%d weight:%d age:%d\n", obj->height, obj->weight, obj->age);
}

int main() {
    Actress a;
    a.height = 168;
    a.weight = 50;
    a.age = 20;

    desc(&a);
    return 0;
}

Redis中AE相關的代碼實現,便是如此。

再看一個C++普通的類:

#include <stdio.h>
class Actress {
public:
    int height; // 身高
    int weight; // 體重
    int age;    // 年齡(注意,這不是數據庫,不必一定存儲生日)

    void desc() {
        printf("height:%d weight:%d age:%d\n", height, weight, age);
    }
};

int main() {
    Actress a;
    a.height = 168;
    a.weight = 50;
    a.age = 20;

    a.desc();
    return 0;
}

你覺得你這個class實際相當于C語言兩種寫法中的哪一個?

看著像寫法一?其實相當于寫法二。C++編譯器實際會幫你生成一個類似上例中C語言寫法二的形式。這也算是C++ zero overhead(零開銷)原則的一個體現。

You shouldn't pay for what you don't use.

當然實際并不完全一致,因為C++支持重載的關系,會存在命名崩壞。但主要思想相同,雖不中,亦不遠矣。

看到這,你會明白:C++中類和操作的封裝只是對于程序員而言的。而編譯器編譯之后其實還是面向過程的代碼。編譯器幫你給成員函數增加一個額外的類指針參數,運行期間傳入對象實際的指針。類的數據(成員變量)和操作(成員函數)其實還是分離的。

每個函數都有地址(指針),不管是全局函數還是成員函數在編譯之后幾乎類似。

在類不含有虛函數的情況下,編譯器在編譯期間就會把函數的地址確定下來,運行期間直接去調用這個地址的函數即可。這種函數調用方式也就是所謂的靜態綁定static binding)。

何謂多態?

虛函數的出現其實就是為了實現面向對象三個特性之一的多態polymorphism)。

#include <stdio.h>
#include <string>
using std::string;
class Actress {
public:
    Actress(int h, int w, int a):height(h),weight(w),age(a){};

    virtual void desc() {
        printf("height:%d weight:%d age:%d\n", height, weight, age);
    }

    int height; // 身高
    int weight; // 體重
    int age;    // 年齡(注意,這不是數據庫,不必一定存儲生日)
};

class Sensei: public Actress {
public:
    Sensei(int h, int w, int a, string c):Actress(h, w, a),cup(c){};
    virtual void desc() {
        printf("height:%d weight:%d age:%d cup:%s\n", height, weight, age, cup.c_str());
    }
    string cup;

};

int main() {
    Sensei s(1685020"36D");

    s.desc();
    return 0;
}

上例子,最終輸出顯而易見:

height:168 weight:50 age:20 cup:36D

再看:

    Sensei s(1685020"36D");

    Actress* a = &s;
    a->desc();

    Actress& a2 = s;
    a2.desc();

這種情況下,用父類指針指向子類的地址,最終調用desc函數還是調用子類的。輸出:

height:168 weight:50 age:20 cup:36D
height:168 weight:50 age:20 cup:36D

這個現象稱之為動態綁定dynamic binding)或者延遲綁定lazy binding)。

但倘若你 把父類Actress中desc()函數前面的vitural去掉,這個代碼最終將調用父類的函數desc(),而非子類的desc()!輸出:

height:168 weight:50 age:20
height:168 weight:50 age:20

這是為什么呢?指針實際指向的還是子類對象的內存空間,可是為什么不能調用到子類的desc()?這個就是我在第一部分說過的:類的數據(成員變量)和操作(成員函數)其實是分離的。

僅從對象的內存布局來看,只能看到成員變量,看不到成員函數。因為調用哪個函數是編譯期間就確定了的,編譯期間只能識別父類的desc()。

好了,現在我們對于C++如何應用多態有了一定的了解,那么多態又是如何實現的呢?

終于我們談到虛表

C++具體多態的實現一般是編譯器廠商自由發揮的。但無獨有偶,使用虛表指針來實現多態幾乎是最常見做法(基本上已經是最好的多態實現方法)。廢話不多說,繼續看代碼,有微調:

#include <stdio.h>
class Actress {
public:
    Actress(int h, int w, int a):height(h),weight(w),age(a){};

    virtual void desc() {
        printf("height:%d weight:%d age:%d\n", height, weight, age);
    }

    virtual void name() {
        printf("I'm a actress");
    }

    int height; // 身高
    int weight; // 體重
    int age;    // 年齡(注意,這不是數據庫,不必一定存儲生日)
};

class Sensei: public Actress {
public:
    Sensei(int h, int w, int a, const char* c):Actress(h, w, a){
        snprintf(cup, sizeof(cup), "%s", c);
    };
    virtual void desc() {
        printf("height:%d weight:%d age:%d cup:%s\n", height, weight, age, cup);
    }
    virtual void name() {
        printf("I'm a sensei");
    }
    char cup[4];

};

int main() {
    Sensei s(1685020"36D");
    s.desc();

    Actress* a = &s;
    a->desc();

    Actress& a2 = s;
    a2.desc();
    return 0;
}

父類有兩個虛函數,子類重載了這兩個虛函數。

clang有個命令可以輸出對象的內存布局(不同編譯器內存布局未必相同,但基本類似):

clang -cc1 -fdump-record-layouts -stdlib=libc++ actress.cpp

可以得到:

*** Dumping AST Record Layout
         0 | class Actress
         0 |   (Actress vtable pointer)
         8 |   int height
        12 |   int weight
        16 |   int age
           | [sizeof=24, dsize=20, align=8,
           |  nvsize=20, nvalign=8]

*** Dumping AST Record Layout
         0 | class Sensei
         0 |   class Actress (primary base)
         0 |     (Actress vtable pointer)
         8 |     int height
        12 |     int weight
        16 |     int age
        20 |   char [4] cup
           | [sizeof=24, dsize=24, align=8,
           |  nvsize=24, nvalign=8]

內存布局、大小、內存對齊都一目了然。

可以發現父類Actress的起始位置多了一個Actress vtable pointer。子類Sensei是在父類的基礎上多了自己的成員cup。

也就是說在含有虛函數的類編譯期間,編譯器會自動給這種類在起始位置追加一個虛表指針,一般稱之為:vptr。vptr指向一個虛表,稱之為:vtablevtbl,虛表中存儲了實際的函數地址。

再看下虛表存儲了什么東西。你在網上搜一下資料,肯定會說虛表里存儲了虛函數的地址,但是其實不止這些!clang同樣有命令:

clang -Xclang -fdump-vtable-layouts -stdlib=libc++ -c actress.cpp

g++也有打印虛表的操作(請在Linux上使用g++),會自動寫到一個文件里:

g++ -fdump-class-hierarchy actress.cpp

看下clang的結果:

Vtable for 'Actress' (4 entries).
   0 | offset_to_top (0)
   1 | Actress RTTI
       -- (Actress, 0) vtable address --
   2 | void Actress::desc()
   3 | void Actress::name()

VTable indices for 'Actress' (2 entries).
   0 | void Actress::desc()
   1 | void Actress::name()

Vtable for 'Sensei' (4 entries).
   0 | offset_to_top (0)
   1 | Sensei RTTI
       -- (Actress, 0) vtable address --
       -- (Sensei, 0) vtable address --
   2 | void Sensei::desc()
   3 | void Sensei::name()

VTable indices for 'Sensei' (2 entries).
   0 | void Sensei::desc()
   1 | void Sensei::name()
   

g++的結果(其實也比較清晰,甚至更清晰):

Vtable for Actress
Actress::_ZTV7Actress: 4u entries
0     (int (*)(...))0
8     (int (*)(...))(& _ZTI7Actress)
16    (int (*)(...))Actress::desc
24    (int (*)(...))Actress::name

Class Actress
   size=24 align=8
   base size=20 base align=8
Actress (0x0x7f9b1fa8c960) 0
    vptr=((& Actress::_ZTV7Actress) + 16u)

Vtable for Sensei
Sensei::_ZTV6Sensei: 4u entries
0     (int (*)(...))0
8     (int (*)(...))(& _ZTI6Sensei)
16    (int (*)(...))Sensei::desc
24    (int (*)(...))Sensei::name

Class Sensei
   size=24 align=8
   base size=24 base align=8
Sensei (0x0x7f9b1fa81138) 0
    vptr=((& Sensei::_ZTV6Sensei) + 16u)
  Actress (0x0x7f9b1fa8c9c0) 0
      primary-for Sensei (0x0x7f9b1fa81138)

可以看出二者其實基本一致,只是個別名稱叫法不同。

所有虛函數的的調用取的是哪個函數(地址)是在運行期間通過查虛表確定的。

更新:vptr指向的并不是虛表的表頭,而是直接指向的虛函數的位置。使用gdb或其他工具可以發現:

(gdb) p s
$2 = {<Actress> = {_vptr.Actress = 0x400a70 <vtable for Sensei+16>, height = 168, weight = 50, age = 20}, cup = "36D"}

vptr指向的是Sensei的vtable + 16個字節的位置,也就是虛表的地址。

虛表本身是連續的內存。動態綁定的實現也就相當于(假設p為含有虛函數的對象指針):

(*(p->vptr)[n])(p)

但其實上面的圖片也只是簡化版,不是完整的的虛表。通過gdb查看,你其實可以發現子類和父類的虛表是連在一起的。上面gdb打印出了虛表指針指向:0x400a70。我們倒退16個字節(0x400a60)輸出一下:

可以發現子類和父類的虛表其實是連續的。并且下面是它們的typeinfo信息也是連續的。

虛表的第一個條目vtable for Sensei值為0。

虛表的第二個條目vtable for Sensei+8指向的其實是0x400ab0,也就是下面的typeinfo for Sensei。

再改一下代碼。我們讓子類Sensei只重載一個父類函數desc()。

class Sensei: public Actress {
public:
    Sensei(int h, int w, int a, const char* c):Actress(h, w, a){
        snprintf(cup, sizeof(cup), "%s", c);
    };
    virtual void desc() {
        printf("height:%d weight:%d age:%d cup:%s\n", height, weight, age, cup);
    }
    char cup[4];

};

其他地方不變,重新用clang或g++剛才的命令執行一遍。clang的輸出:

Vtable for 'Actress' (4 entries).
   0 | offset_to_top (0)
   1 | Actress RTTI
       -- (Actress, 0) vtable address --
   2 | void Actress::desc()
   3 | void Actress::name()

VTable indices for 'Actress' (2 entries).
   0 | void Actress::desc()
   1 | void Actress::name()

Vtable for 'Sensei' (4 entries).
   0 | offset_to_top (0)
   1 | Sensei RTTI
       -- (Actress, 0) vtable address --
       -- (Sensei, 0) vtable address --
   2 | void Sensei::desc()
   3 | void Actress::name()

VTable indices for 'Sensei' (1 entries).
   0 | void Sensei::desc()
   

可以看到子類的name由于沒有重載,所以使用的還是父類的。一圖勝千言:

好了,寫了這么多,相信大家應該已經能理解虛表存在的意義及其實現原理。但同時我也埋下了新的坑沒有填:

虛表中的前兩個條目是做什么用的?

它倆其實是為多重繼承服務的。

  • 第一個條目存儲的offset,是一種被稱為thunk的技術(或者說技巧)。
  • 第二個條目存儲著為RTTI服務的type_info信息。

關于這部分的介紹,請關注后續文章!


往期推薦



研究了一下Android JNI,有幾個知識點不太懂。

介紹一個C++中非常有用的設計模式

60 張圖詳解 98 個常見網絡概念

沒辦法,基因決定的!

哪家互聯網公司一周工作時間最長??太卷了?。。?/p>

C++的lambda是函數還是對象?

深入理解glibc malloc:內存分配器實現原理

到底什么是掛載?

C/C++為什么要專門設計個do…while?

為什么空類大小是1

推薦一個學習技術的好網站

在部隊當程序員有多爽?

Linux最大并發數是多少?

C++ protected繼承和private繼承是不是沒用的廢物?

累夠嗆!整理了一份C++學習路線圖!

圖解|工作6年多,我還是沒有搞懂什么是協程的道與術

研究了一波Android Native C++內存泄漏的調試

參加了 40 多場面試。

如何調試內存泄漏?方法論來了

清華大學:2021 元宇宙研究報告!


主站蜘蛛池模板: 田林县| 巴林右旗| 蓬莱市| 岱山县| 江阴市| 邵东县| 永康市| 商水县| 巴青县| 佛教| 蒲城县| 宿州市| 古浪县| 扎囊县| 兴安县| 海安县| 北京市| 信阳市| 洛川县| 汾阳市| 福安市| 富宁县| 娱乐| 青铜峡市| 宁阳县| 尤溪县| 乌拉特后旗| 马尔康县| 龙海市| 宜兰县| 体育| 嘉禾县| 彰武县| 安国市| 宝鸡市| 温州市| 江川县| 横山县| 内黄县| 财经| 玉龙|