虛函數(shù)卸亮、虛表與多繼承

經(jīng)常在編譯錯誤中看到的vTable究竟是什么忽妒?

為什么要有虛函數(shù)

C++的設(shè)計理念是:用不到的功能就不要在運(yùn)行時花費(fèi)時間。正因如此,C++中會有靜態(tài)綁定段直、動態(tài)綁定吃溅、虛函數(shù)這些概念。

對比其他一些面向?qū)ο蟮恼Z言鸯檬,可以認(rèn)為它們所有成員函數(shù)都是虛函數(shù)决侈,都是動態(tài)綁定,而C++則為了性能考慮喧务,只有實(shí)際用到時赖歌,即成員函數(shù)有virtual修飾,才開啟動態(tài)綁定蹂楣。

靜態(tài)綁定與動態(tài)綁定

所謂靜態(tài)綁定俏站,是指成員函數(shù)的地址在編譯期就可以確定讯蒲,運(yùn)行時則直接跳轉(zhuǎn)到對應(yīng)地址執(zhí)行痊土;而動態(tài)綁定則是指編譯期不確定,只有到運(yùn)行時才能找到函數(shù)地址墨林,這需要兩次額外的尋址指令:第1次找到虛表赁酝,第2次從虛表中找到函數(shù)地址。

哪些情況會出現(xiàn)動態(tài)綁定旭等?答案是只有使用指針或引用調(diào)用虛成員函數(shù)時才會出現(xiàn)酌呆。

例如:

class VirtualClass {
public:
  virtual void f() {}
};

class SubClass : public VirtualClass {
  virtual void f() override {}
};

int main() {
  auto* p = new SubClass();
  p->f(); // 動態(tài)綁定
}

之所以不能用靜態(tài)綁定,是因?yàn)閜不僅可以指向VirtualClass對象搔耕,也可以指向它的子類SubClass 隙袁,而我們在編譯期并不能確定它具體指向哪個類。

內(nèi)存布局與虛表指針

在分析虛表之前弃榨,先討論一下類的內(nèi)存布局菩收。

一個類實(shí)例需要占據(jù)一定的內(nèi)存空間,而空間的大小以及其中內(nèi)容的排布鲸睛,對同一個類的所有實(shí)例都是相同的娜饵。例如下面這個類:

class Layout {
public:
    short s;
    int i;
    long long int l;
    void f();
};

在Visual Studio的工具中可以看到它的內(nèi)存排布:


而當(dāng)我們把成員函數(shù)f改成虛函數(shù)時:

class Layout {
public:
    short s;
    int i;
    long long int l;
    virtual void f();
};

我們發(fā)現(xiàn),前8個字節(jié)增加了一個指針 vfptr 即虛表指針官辈,它指向一個虛表箱舞。從另一個角度,我們也可以理解為拳亿,Layout類的前8個字節(jié)用來標(biāo)識它的實(shí)際類型(Layout或其某個子類)晴股。

虛表

前面說到了虛表,那么虛表到底是什么呢肺魁?

虛表就是一個數(shù)組电湘,它存儲了一系列函數(shù)指針。只有包含虛函數(shù)的類才會有虛表,一個類的所有實(shí)例公用一個虛表胡桨。虛表中的每個指針則是指向這個類的所有虛函數(shù)官帘。

下面的代碼和對應(yīng)的示意圖可以看得很清楚:

class Instrument {
public:
    virtual void play() {};
    virtual void adjust() {};
};

class Wind : public Instrument {
public:
    virtual void play() override { printf("Wind play"); }
    virtual void adjust() override { printf("Wind adjust"); }
    int score = 1;
};

class Brass : public Wind {
public:
    virtual void play() override { printf("Brass play"); }
    virtual void what() { printf("Brass what"); }
    int score = 2;
};

int main() {
    Instrument* list[4];
    list[0] = new Instrument();
    list[1] = new Wind();
    list[2] = new Brass();
    list[3] = new Brass();
    return 0;
}

從這個例子中可以看到,當(dāng)子類重寫(override)父類的虛函數(shù)時昧谊,虛表中的對應(yīng)指針也會修改刽虹,但順序不變。當(dāng)子類新增虛函數(shù)時呢诬,則會在虛表末尾新增涌哲。

按照這樣的規(guī)則,當(dāng)我們把子類的指針或引用向上類型轉(zhuǎn)換時尚镰,它的虛表完全可以當(dāng)做時父類的虛表來使用阀圾,無需關(guān)心實(shí)際類型。

多繼承

上面說到狗唉,子類的內(nèi)存布局和虛表都兼容父類初烘,但這時又出現(xiàn)一個新的問題,如果有多繼承怎么辦分俯?如何同時兼容兩個父類呢肾筐?

其實(shí),多繼承的情況缸剪,子類會的內(nèi)存布局會將兩個父類依次排布吗铐,也就是會有兩個虛表指針。

例如:

class Flyable {
public:
    virtual void fly() {}
    int hight = 0;
};

class Runnable {
public:
    virtual void run() {}
    int speed = 0;
};

class Bird : public Flyable, public Runnable {
public:
    virtual void fly() override {}
    virtual void run() override {}
    virtual void eat() {}
    int weight = 0;
};

Bird的內(nèi)存布局如下:

從圖上可以清楚看到杏节,0x00 ~ 0x0f 這部分內(nèi)存布局兼容Flyable類唬渗,0x10 ~ 0x1f 兼容,0x20之后的地址是Bird類自己的成員變量奋渔。

而Bird類自己的虛成員函數(shù) eat() 會加在哪個虛表里呢镊逝?答案是加到第一個虛表中,和單繼承的情況類似卒稳。

指針偏移

這時你可能又要問了蹋半,當(dāng)Bird類型指針向上轉(zhuǎn)換成Runnable指針之后,再調(diào)用虛函數(shù)時充坑,又怎么知道此時應(yīng)該去0x10的位置减江,而非0x00找虛表指針呢?

答案是捻爷,不需要辈灼。因?yàn)樵谧鲱愋娃D(zhuǎn)換的時候,會直接將指針偏移到0x10的位置也榄,我們來驗(yàn)證一下:

int main() {
    Bird* b = new Bird();
    Runnable* r = b;
    printf("b: %x\nr: %x\nb == r: %d", b, r, b == r);
}

output:
b: ee617fb0
r: ee617fc0
b == r: 1

可以看到巡莹,b和r的地址確實(shí)不同司志,但當(dāng)我們做比較運(yùn)算時,結(jié)果卻是相等降宅。所以大多數(shù)時候骂远,我們不需要關(guān)注這里的指針偏移。

但這樣一來腰根,也存在一個坑激才,就是我們不能將Bird*類型先轉(zhuǎn)成void*之后,再強(qiáng)轉(zhuǎn)成Runnable*類型额嘿,因?yàn)檫@樣的轉(zhuǎn)換不會做指針偏移瘸恼。

對于包含虛表的類,做類型轉(zhuǎn)換時一般用dynamic_cast册养,但不支持void*东帅。

還是以上面的繼承關(guān)系為例:

int main() {
    Bird* b = new Bird();
    Flyable* f = b;
    void* v = b;
    printf("sc: %x\ndc: %x", static_cast<Runnable*>(v), dynamic_cast<Runnable*>(f));
}

output:
sc: 96079740
dc: 96079750

可以看到, vf實(shí)際指向的時同一個 Bird對象球拦,但兩種類型轉(zhuǎn)換后指針卻不同靠闭,就是因?yàn)?static_castvoid*轉(zhuǎn)換到 Runnable 時沒有做指針偏移。而 dynamic_cast會動態(tài)檢查對象的實(shí)際類型刘莹,所以總能做出正確的指針偏移阎毅。

更多思考

就到此為止了嗎焚刚?其實(shí)還有其他更復(fù)雜的情況点弯,例如多繼承時,兩個父類包含相同簽名的虛函數(shù)矿咕;例如有菱形繼承抢肛、虛繼承的情況。這些復(fù)雜情況在實(shí)際應(yīng)用中較少碰到碳柱,就不做詳細(xì)討論了捡絮。

另外再提一下,C++中沒有“虛成員變量”莲镣,當(dāng)我們做向上類型轉(zhuǎn)換后福稳,就無法直接獲取到子類的成員變量了,只能通過虛函數(shù)來獲取瑞侮。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末的圆,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子半火,更是在濱河造成了極大的恐慌越妈,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,607評論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件钮糖,死亡現(xiàn)場離奇詭異梅掠,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,239評論 3 395
  • 文/潘曉璐 我一進(jìn)店門阎抒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來酪我,“玉大人,你說我怎么就攤上這事且叁〖朗荆” “怎么了?”我有些...
    開封第一講書人閱讀 164,960評論 0 355
  • 文/不壞的土叔 我叫張陵谴古,是天一觀的道長质涛。 經(jīng)常有香客問我,道長掰担,這世上最難降的妖魔是什么汇陆? 我笑而不...
    開封第一講書人閱讀 58,750評論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮带饱,結(jié)果婚禮上毡代,老公的妹妹穿的比我還像新娘。我一直安慰自己勺疼,他們只是感情好教寂,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,764評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著执庐,像睡著了一般酪耕。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上轨淌,一...
    開封第一講書人閱讀 51,604評論 1 305
  • 那天迂烁,我揣著相機(jī)與錄音,去河邊找鬼递鹉。 笑死盟步,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的躏结。 我是一名探鬼主播却盘,決...
    沈念sama閱讀 40,347評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼媳拴!你這毒婦竟也來了黄橘?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,253評論 0 276
  • 序言:老撾萬榮一對情侶失蹤禀挫,失蹤者是張志新(化名)和其女友劉穎旬陡,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體语婴,經(jīng)...
    沈念sama閱讀 45,702評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡描孟,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,893評論 3 336
  • 正文 我和宋清朗相戀三年驶睦,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片匿醒。...
    茶點(diǎn)故事閱讀 40,015評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡场航,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出廉羔,到底是詐尸還是另有隱情溉痢,我是刑警寧澤,帶...
    沈念sama閱讀 35,734評論 5 346
  • 正文 年R本政府宣布憋他,位于F島的核電站孩饼,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏竹挡。R本人自食惡果不足惜镀娶,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,352評論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望揪罕。 院中可真熱鬧梯码,春花似錦、人聲如沸好啰。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,934評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽框往。三九已至鳄抒,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間搅窿,已是汗流浹背嘁酿。 一陣腳步聲響...
    開封第一講書人閱讀 33,052評論 1 270
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留男应,地道東北人。 一個月前我還...
    沈念sama閱讀 48,216評論 3 371
  • 正文 我出身青樓娱仔,卻偏偏與公主長得像沐飘,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子牲迫,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,969評論 2 355