C++對(duì)象模型1——虛函數(shù)表和虛函數(shù)指針

為什么要了解C++的對(duì)象模型艰管,我覺得第一點(diǎn)就是了解了C++的對(duì)象模型之后可以避免很多C++的錯(cuò)誤寫法闷游;第二點(diǎn)可以加深對(duì)C++編譯器的理解镊尺,了解C++編譯器在編譯的時(shí)候做了哪些幕后工作悲立;第三點(diǎn)是可以加強(qiáng)對(duì)計(jì)算機(jī)底層的理解,并且可以可以增加對(duì)C語言的理解档冬。文中的內(nèi)容膘茎,是我讀了《深度探索C++對(duì)象模型》這本書之后的一點(diǎn)心得體會(huì)。所有的例子的運(yùn)行環(huán)境是VS2013,64位酷誓,Windows10系統(tǒng)披坏。

如果父類有虛函數(shù),那么子類可以覆寫父類的虛函數(shù)盐数,達(dá)到多態(tài)的效果棒拂。但問題是,在編譯器編譯的時(shí)候不能確定該調(diào)用哪一個(gè)函數(shù)娘扩,所以只能在運(yùn)行時(shí)確定着茸。要在運(yùn)行時(shí)確定壮锻,類對(duì)象里面就必須有虛函數(shù)地址的信息。很明顯涮阔,如果把每個(gè)虛函數(shù)的地址都存入對(duì)象中猜绣,是很浪費(fèi)的,所以對(duì)象里面僅僅保存一個(gè)指向一個(gè)含有類中所有虛函數(shù)地址的指針敬特,這個(gè)地址就是虛函數(shù)表掰邢。相同類對(duì)象的虛函數(shù)表是相同的,所以每個(gè)類的虛函數(shù)表在內(nèi)存中只有一份伟阔。虛函數(shù)表和指針都是在編譯器編譯的時(shí)候加進(jìn)去的辣之,不同的編譯器在實(shí)現(xiàn)細(xì)節(jié)上有差別,這里以VS2013為例皱炉。

虛函數(shù)指針的位置

按常理來說怀估,虛函數(shù)指針不可能放在類對(duì)象的中間,只能放在頭部和尾部合搅。這樣就比較好確定了多搀,如下的代碼就可以區(qū)分是否在頭部。

#include<iostream>
using namespace std;

class A{
public:
    int val;
    virtual void func(){
        cout << "A::func()" << endl;
    }
};

int main(){
    A a;
    char *p1 = (char *)&a;
    char *p2 = (char *)&a.val;
    if (p1 != p2){
        cout << "start" << endl;
    }
    else{
        cout << "end" << endl;
    }
    return 0;
}

在VS2013中灾部,虛函數(shù)指針被放在了對(duì)象的頭部康铭。

通過虛函數(shù)地址調(diào)用虛函數(shù)

先看下面的代碼:

#include<iostream>
using namespace std;

class A{
public:
    int val = 100;
    virtual void f1(){
        cout << "A::f1()" << endl;
    }
    virtual void f2(){
        cout << "A.val = " << val << endl;
    }
};

typedef void(*func)(A *a);

int main(){
    A a;
    func** pvptr = (func **)&a;
    func* vptr = *pvptr;
    for (int i = 0; i < 2; i++){
        vptr[i](&a);
    }
    return 0;
}

為了簡(jiǎn)單起見,這里虛函數(shù)的參數(shù)和返回值都是一樣的赌髓,值得注意的是从藤,如果使用這種方式調(diào)用函數(shù),第一個(gè)參數(shù)需要傳入this指針锁蠕,不然會(huì)出問題夷野。虛函數(shù)表是類型為func的數(shù)組,而a的首地址存的是func*類型的匿沛,再對(duì)a取地址扫责,得到的是func**類型。運(yùn)行結(jié)果表明逃呼,在虛函數(shù)表中虛函數(shù)的排列是按照虛函數(shù)的聲明順序來排列的鳖孤,這也是符合一般的習(xí)慣。

可以使用VS2013中提供的命令行提示工具來查看生成類的布局抡笼,在命令行提示工具中輸入:

 /* cl /d1 reportSingleClassLayout類名 源文件名 */
cl /d1 reportSingleClassLayoutA virtual_function.cpp

就可以打印出類A的相關(guān)信息苏揣。在Linux下,也有類似的命令:

g++ -fdump -class -hierarchy -fsyntax -only 源文件名

在輸出中可以看到如下的內(nèi)容:

class A size(16):
        +---
 0      | {vfptr}
 8      | val
        | <alignment member> (size=4)
        +---

A::$vftable@:
        | &A_meta
        |  0
 0      | &A::f1
 1      | &A::f2

可以看出類A的布局的確如上面所說推姻。

靜態(tài)聯(lián)編和動(dòng)態(tài)聯(lián)編

簡(jiǎn)單的說平匈,靜態(tài)聯(lián)編就是在編譯的時(shí)候就確定函數(shù)的調(diào)用地址,而動(dòng)態(tài)聯(lián)編就是在運(yùn)行時(shí)在確定函數(shù)調(diào)用的地址。在C++中實(shí)現(xiàn)動(dòng)態(tài)聯(lián)編需要同時(shí)滿足以下三個(gè)條件:虛函數(shù)增炭,繼承關(guān)系忍燥,基類指針或引用指向子類對(duì)象。這會(huì)導(dǎo)致下面的區(qū)別:

#include<iostream>
using namespace std;

class A{
public:
    int val = 100;
    virtual void f(){
        cout << "A::f()" << endl;
    }
};

class B :public A{
public:
    virtual void f() override{
        cout << "B::f()" << endl;
    }
};

typedef void(*func)();

int main(){
    B b;
    A a = b;
    A *pa = &b;
    a.f();
    pa->f();

    func **vpa = (func **)&b;
    func *va = *vpa;
    (*va)();

    return 0;
}

運(yùn)行程序隙姿,會(huì)發(fā)現(xiàn)以下的現(xiàn)象梅垄,使用a.f()調(diào)用的父類的方法,而直接使用虛函數(shù)調(diào)用還是調(diào)用的是父類的方法输玷。說明虛函數(shù)指針在賦值的時(shí)候也是被修改了的队丝。通過反匯編,可以發(fā)現(xiàn)欲鹏,使用a.f()調(diào)用的時(shí)候是直接跳轉(zhuǎn)到函數(shù)地址机久,而使用pa->f()調(diào)用需要的步驟更多,說明pa->f()是動(dòng)態(tài)聯(lián)編赔嚎,在運(yùn)行的是才決定調(diào)用哪個(gè)函數(shù)膘盖。

    a.f();
00007FF7523B4B2D  lea         rcx,[a]  
00007FF7523B4B32  call        A::f (07FF7523B1190h)  
    pa->f();
00007FF7523B4B37  mov         rax,qword ptr [pa]  
00007FF7523B4B3C  mov         rax,qword ptr [rax]  
00007FF7523B4B3F  mov         rcx,qword ptr [pa]  
00007FF7523B4B44  call        qword ptr [rax]  

虛函數(shù)指針有關(guān)的錯(cuò)誤

先看下面一個(gè)程序:

#include<iostream>
using namespace std;

class A{
public:
    int val;
    virtual void f(){
        cout << "A::f()" << endl;
    }
    A(){
        memset(this, 0, sizeof(A));
    }

    A(const A& obj){
        memcpy(this, &obj, sizeof(A));
    }
};

class B :public A{
public:
    virtual void f() override{
        cout << "B::f()" << endl;
    }
};

int main(){
    
    A *a1 = new A();
    //a1->f();
    B b;
    A a(b);
    a.f();
    A *p = &a;
    p->f();

    return 0;
}

這個(gè)程序有兩個(gè)問題,第一個(gè)尽狠,a1->f()會(huì)出現(xiàn)錯(cuò)誤衔憨,第二,a.f()調(diào)用的是A的函數(shù)袄膏,p->f()調(diào)用的是B的函數(shù)掺冠。其實(shí)是使用memset會(huì)把虛函數(shù)指針清空沉馆,而memcpy會(huì)把虛函數(shù)指針也賦值德崭。第一個(gè)a.f()因?yàn)槭庆o態(tài)聯(lián)編,沒有經(jīng)過虛函數(shù)指針眉厨。

如果出現(xiàn)了這種奇怪的錯(cuò)誤锌奴,不懂虛函數(shù)指針,會(huì)覺得非常奇怪憾股。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市服球,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌斩熊,老刑警劉巖往枣,帶你破解...
    沈念sama閱讀 210,978評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡分冈,警方通過查閱死者的電腦和手機(jī)圾另,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門盯捌,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人饺著,你說我怎么就攤上這事∮姿ィ” “怎么了缀雳?”我有些...
    開封第一講書人閱讀 156,623評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵渡嚣,是天一觀的道長(zhǎng)肥印。 經(jīng)常有香客問我,道長(zhǎng)深碱,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,324評(píng)論 1 282
  • 正文 為了忘掉前任功咒,我火速辦了婚禮,結(jié)果婚禮上力奋,老公的妹妹穿的比我還像新娘幽七。我一直安慰自己景殷,他們只是感情好澡屡,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,390評(píng)論 5 384
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著亭饵,像睡著了一般。 火紅的嫁衣襯著肌膚如雪辜羊。 梳的紋絲不亂的頭發(fā)上踏兜,一...
    開封第一講書人閱讀 49,741評(píng)論 1 289
  • 那天八秃,我揣著相機(jī)與錄音,去河邊找鬼昔驱。 笑死,一個(gè)胖子當(dāng)著我的面吹牛纳本,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播繁成,決...
    沈念sama閱讀 38,892評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼巾腕!你這毒婦竟也來了絮蒿?” 一聲冷哼從身側(cè)響起尊搬,我...
    開封第一講書人閱讀 37,655評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤土涝,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后回铛,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,104評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡茵肃,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年袭祟,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片巾乳。...
    茶點(diǎn)故事閱讀 38,569評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖氨鹏,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情仆抵,我是刑警寧澤跟继,帶...
    沈念sama閱讀 34,254評(píng)論 4 328
  • 正文 年R本政府宣布镣丑,位于F島的核電站舔糖,受9級(jí)特大地震影響莺匠,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜趣竣,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,834評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望卫袒。 院中可真熱鬧,春花似錦玛臂、人聲如沸封孙。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,725評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至膜蠢,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間挑围,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,950評(píng)論 1 264
  • 我被黑心中介騙來泰國(guó)打工模捂, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人狂男。 一個(gè)月前我還...
    沈念sama閱讀 46,260評(píng)論 2 360
  • 正文 我出身青樓品腹,卻偏偏與公主長(zhǎng)得像岖食,于是被迫代替她去往敵國(guó)和親舞吭。 傳聞我的和親對(duì)象是個(gè)殘疾皇子析珊,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,446評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容