亚洲欧美第一页_禁久久精品乱码_粉嫩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語言的項目中你很少會看到這種寫法,主要原因就是函數指針是有空間成本的,這樣寫的話每個實例化的對象中都會有一個指針大小(比如8字節)的空間占用,如果實例化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 個常見網絡概念

沒辦法,基因決定的!

哪家互聯網公司一周工作時間最長??太卷了!!!

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

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

到底什么是掛載?

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

為什么空類大小是1

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

在部隊當程序員有多爽?

Linux最大并發數是多少?

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

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

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

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

參加了 40 多場面試。

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

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


亚洲欧美第一页_禁久久精品乱码_粉嫩av一区二区三区免费野_久草精品视频
欧美日韩免费看| 亚洲高清视频一区二区| 国产日韩欧美日韩| 日韩视频在线永久播放| 免费中文日韩| 亚洲精品中文字幕在线| 欧美日本国产一区| 亚洲日本中文字幕| 国产精品白丝黑袜喷水久久久| 一区二区三区视频在线| 国产精品任我爽爆在线播放 | 久久天堂av综合合色| 狠狠色伊人亚洲综合成人| 久久久国际精品| 亚洲精品一区二区三区在线观看| 欧美美女喷水视频| 性欧美办公室18xxxxhd| 亚洲国产综合在线看不卡| 欧美日韩国产成人在线91| 亚洲视频国产视频| 国内精品免费午夜毛片| 欧美精品午夜视频| 亚洲欧美激情视频在线观看一区二区三区| 国产亚洲一区二区精品| 欧美18av| 午夜精品福利视频| 亚洲毛片视频| 亚洲国产成人在线播放| 国产精品自拍一区| 欧美三级第一页| 欧美中文字幕| 亚洲一级在线观看| 在线观看日韩av先锋影音电影院| 欧美日韩在线大尺度| 麻豆成人在线播放| 亚洲欧美日韩国产成人精品影院| 亚洲第一黄色网| 国产模特精品视频久久久久| 欧美日韩免费观看一区二区三区| 亚洲综合视频1区| 精品成人国产| 国产在线精品二区| 国产精品激情av在线播放| 毛片av中文字幕一区二区| 久久国产精品一区二区三区| 午夜国产精品视频免费体验区| 亚洲精品国产精品国产自| 精品电影一区| 国产亚洲欧美激情| 国产乱肥老妇国产一区二| 国产精品欧美风情| 国产乱码精品一区二区三区忘忧草| 欧美日韩免费看| 欧美日韩另类视频| 欧美天堂亚洲电影院在线播放| 欧美精品久久99久久在免费线| 免费在线日韩av| 免费在线播放第一区高清av| 暖暖成人免费视频| 欧美激情国产日韩| 欧美激情亚洲国产| 欧美国产先锋| 美女久久网站| 欧美精品网站| 国产精品私拍pans大尺度在线| 国产精品你懂的在线欣赏| 国模精品一区二区三区| 在线观看国产精品淫| 亚洲日本成人| 一区二区三区久久| 欧美伊人久久大香线蕉综合69| 久久成人综合网| 久久久综合精品| 欧美国产视频一区二区| 欧美日本在线看| 欧美金8天国| 欧美激情一区二区| 国产精品久久久亚洲一区 | 99精品欧美| 亚洲精品一区在线观看香蕉| 亚洲精品国产精品国自产观看| 狠狠色丁香久久婷婷综合_中| 国内自拍一区| 99re在线精品| 亚洲综合日韩中文字幕v在线| 久久精品国产一区二区电影| 欧美一区国产在线| 免费视频亚洲| 国产精品v日韩精品v欧美精品网站| 欧美午夜无遮挡| 黑人巨大精品欧美黑白配亚洲 | 1024成人| 日韩视频一区| 99精品视频免费| 亚洲欧美国产制服动漫| 另类春色校园亚洲| 国产精品久久久久久久浪潮网站| 国产欧美日韩综合精品二区| 精品成人a区在线观看| 9色porny自拍视频一区二区| 欧美伊久线香蕉线新在线| 欧美成人午夜免费视在线看片 | 一本一本久久a久久精品综合妖精| 亚洲黄网站在线观看| 亚洲调教视频在线观看| 欧美自拍偷拍午夜视频| 欧美日韩一区二区三区| 亚洲韩国青草视频| 欧美在线观看视频一区二区| 欧美大片免费观看| 黄色成人在线| 亚洲免费在线播放| 欧美a级片网站| 激情伊人五月天久久综合| 中文日韩在线| 欧美国产一区在线| 精品69视频一区二区三区| 欧美一级在线视频| 欧美日韩综合久久| 亚洲精品视频在线观看免费| 久久精品成人一区二区三区| 国产精品久久久久久久久久尿| 国产一区二区三区高清 | 国产精品国产| 亚洲人成网站精品片在线观看| 午夜精品久久久久| 久久精品一本久久99精品| 欧美天天视频| 亚洲免费激情| 欧美日本韩国在线| 亚洲激情女人| 欧美肥婆在线| 亚洲精品国产精品久久清纯直播| 久久久久久久97| 精品成人乱色一区二区| 欧美在线一二三四区| 国产精品一区久久久久| 亚洲综合日韩在线| 国产精品国产三级国产普通话蜜臀| 欧美国产第二页| 国产午夜精品一区二区三区欧美 | 国产精品天美传媒入口| 麻豆国产va免费精品高清在线| 国产综合视频| 99精品视频免费全部在线| 欧美激情按摩| 亚洲三级免费观看| 欧美日韩国产一区二区| 亚洲视频axxx| 国产日韩欧美综合精品| 噜噜噜91成人网| 亚洲精品国产视频| 国产精品www994| 欧美亚洲免费高清在线观看| 精品999成人| 免费亚洲电影| 亚洲一区二区三区在线播放| 国产偷自视频区视频一区二区| 久久美女性网| 中文久久乱码一区二区| 国产精品揄拍一区二区| 久久夜色精品国产| 亚洲国产一二三| 国产精品久久久久影院色老大| 欧美亚洲综合久久| 最新国产拍偷乱拍精品| 国产精品免费一区豆花| 久久青青草原一区二区| 亚洲社区在线观看| 国产亚洲欧美日韩美女| 久久久一区二区三区| 亚洲国产一区二区视频| 国产精品亚洲片夜色在线| 黄色另类av| 国产精品扒开腿爽爽爽视频| 久久久久国产精品一区三寸| 夜夜嗨av一区二区三区四季av| 国产女主播在线一区二区| 久久综合中文色婷婷| 亚洲免费小视频| 亚洲第一网站| 国产视频久久久久| 欧美日韩国产不卡| 久久综合九色综合网站| 亚洲午夜在线观看| 亚洲国产成人av| 国产一区二区三区自拍| 欧美日韩国产综合新一区| 乱人伦精品视频在线观看| 中文av一区特黄| 亚洲精品偷拍| 亚洲成人自拍视频| 国产精品一区二区久久国产| 欧美理论大片| 麻豆国产精品777777在线 | 欧美一区二区三区在线免费观看| 亚洲欧洲一区二区在线观看| 国产日本亚洲高清| 国产精品一区二区在线| 欧美日韩极品在线观看一区|