自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

原來(lái) C++ 虛函數(shù)是這樣實(shí)現(xiàn)的!

開發(fā)
本文讓我們拋開枯燥的概念講解,通過(guò)一個(gè)有趣的故事,一步步揭開 C++ 對(duì)象模型的神秘面紗。

"為什么我的程序這么占內(nèi)存?" 

"虛函數(shù)到底是怎么實(shí)現(xiàn)的?" 

"多態(tài)背后的原理是什么?" 

如果你也有這些疑問(wèn),那么這篇文章正是為你準(zhǔn)備的。讓我們拋開枯燥的概念講解,通過(guò)一個(gè)有趣的故事,一步步揭開 C++ 對(duì)象模型的神秘面紗。

C++ 對(duì)象模型

"誒,小王?。? 老張端著冒著熱氣的咖啡杯走進(jìn)辦公室,眼睛里閃著神秘的光 ?? "今天咱們來(lái)聊個(gè)有意思的歷史故事~"

小王從鍵盤上抬起頭,一臉困惑:"歷史?這和代碼有什么關(guān)系?" ??

老張神秘地笑了笑:"你猜猜看,咱們天天用的 C++,最開始是怎么實(shí)現(xiàn)的?"

"這還用猜嗎?" 小王信心滿滿,"肯定是直接編譯成機(jī)器碼啊!" ??

"嘿嘿,不對(duì)哦~" 老張喝了口咖啡,露出高深莫測(cè)的笑容,"C++ 最初其實(shí)是個(gè)'翻譯官',叫 Cfront,它的工作就是把 C++ 代碼翻譯成 C 代碼!" ??

"不會(huì)吧!" 小王瞪大了眼睛 ?? "這不是多此一舉嗎?"

老張搖搖手指:"聰明著呢!Stroustrup 大師當(dāng)年在貝爾實(shí)驗(yàn)室可是深謀遠(yuǎn)慮啊。你想啊,C 編譯器都已經(jīng)很成熟了,何必重復(fù)造輪子呢?而且這樣一來(lái),C++ 代碼還能和 C 代碼愉快地玩耍,簡(jiǎn)直是一箭雙雕!" ??

"來(lái)來(lái)來(lái)," 老張站起身,走到白板前,"讓我給你變個(gè)魔術(shù),看看 C++ 代碼是怎么'變身'的~" ?

老張揮舞著馬克筆,在白板上畫出一段優(yōu)雅的 C++ 代碼:

// 這是一個(gè)典型的 C++ 類定義 ???
class Rectangle {
public:
    // 構(gòu)造函數(shù),初始化寬和高 ?
    Rectangle(int w, int h) : width(w), height(h) {}
    
    // 計(jì)算面積的成員函數(shù) ??
    int area() { return width * height; }
    
private:
    int width;   // 矩形的寬 ??
    int height;  // 矩形的高 ??
};

"這段 C++ 代碼經(jīng)過(guò)編譯器處理后,本質(zhì)上會(huì)變成這樣的 C 代碼" 老張微笑著說(shuō)

// C 語(yǔ)言結(jié)構(gòu)體定義 ??
struct Rectangle {
    int width;   // 存儲(chǔ)寬度
    int height;  // 存儲(chǔ)高度
};

// 構(gòu)造函數(shù)被轉(zhuǎn)換為普通的 C 函數(shù) ???
void Rectangle_Rectangle(
    struct Rectangle* this, 
    int w, 
    int h
) {
    this->width = w;
    this->height = h;
}

// 成員函數(shù)也變成普通的 C 函數(shù) ??
int Rectangle_area(struct Rectangle* this) {
    returnthis->width * this->height;
}

小王恍然大悟:"原來(lái)如此!C++ 的類其實(shí)就是在 C 的基礎(chǔ)上做了語(yǔ)法糖啊!"

老張點(diǎn)點(diǎn)頭:"是的!所有的成員函數(shù)都會(huì)被轉(zhuǎn)換成普通的 C 函數(shù),只是多了一個(gè) this 指針參數(shù)。這就是為什么我們說(shuō),要真正理解 C++,必須先理解 C 語(yǔ)言的基礎(chǔ)。" 

"那繼承是怎么實(shí)現(xiàn)的呢?" 小王充滿好奇地問(wèn)道 ??

老張笑著說(shuō) ??:"這個(gè)問(wèn)題問(wèn)得好!讓我們繼續(xù)深入了解 C++ 對(duì)象模型的奧秘..."

簡(jiǎn)單對(duì)象模型

"來(lái),小王," 老張放下熱騰騰的咖啡 ??,拿起馬克筆 ??,"讓我們來(lái)了解一下 C++ 最初的對(duì)象模型是什么樣的。這個(gè)模型雖然簡(jiǎn)單,但對(duì)理解繼承特別有幫助哦!" ??

小王立刻坐直了身體,眼睛閃閃發(fā)亮?

"知道為什么現(xiàn)在的 C++ 類寫起來(lái)這么優(yōu)雅嗎?" 老張神秘地笑著說(shuō) ??♂?,"這還得從上古時(shí)代說(shuō)起..."

老張?jiān)诎装迳蠈懴乱欢未a ????:

// 一個(gè)簡(jiǎn)單的學(xué)生類 ????
class Student {
    std::string name;     // 存儲(chǔ)學(xué)生姓名 ??
    int score;           // 記錄考試分?jǐn)?shù) ??
    void study();       // 學(xué)習(xí)方法 ??
    void doHomework();  // 寫作業(yè)功能 ??
};

"猜猜看,在 C++ 剛誕生的年代,Stroustrup 大師是怎么實(shí)現(xiàn)這個(gè)類的?" 老張眨眨眼 ??

小王搖搖頭 ??:"難道...和現(xiàn)在不一樣嗎?"

"哈哈,那時(shí)候?yàn)榱撕?jiǎn)化編譯器的實(shí)現(xiàn),他們?cè)O(shè)計(jì)了'簡(jiǎn)單對(duì)象模型'" ??

老張畫出了內(nèi)部實(shí)現(xiàn)示意圖 ??:

// 在內(nèi)存中的實(shí)際表示
struct Student_Internal {
    void* name_ptr;      // 指向?qū)嶋H的 string 數(shù)據(jù)
    void* score_ptr;     // 指向?qū)嶋H的 int 數(shù)據(jù)
    void* study_ptr;     // 指向 study 函數(shù)
    void* homework_ptr;  // 指向 homework 函數(shù)
};

"看到?jīng)]?" 老張指著圖說(shuō) ??,"所有成員,不管是數(shù)據(jù)還是函數(shù),統(tǒng)統(tǒng)變成指針!就像一個(gè)巨大的導(dǎo)航表 ??? ??"

小王瞪大了眼睛 ??:"等等...那豈不是一個(gè)簡(jiǎn)單的 int 也要用指針來(lái)存?" ??

"沒(méi)錯(cuò)!" 老張笑著說(shuō) ??,"想象一下,你點(diǎn)外賣 ??,每個(gè)菜都要先送到隔壁小區(qū),然后給你一張地址條 ??,告訴你:'你的紅燒肉在A棟3層 ??,炒青菜在B棟5層...' ?? ??♂?"

"這...這不是很浪費(fèi)嗎?" 小王忍不住笑了 ??

"所以啊!" 老張喝了口咖啡 ??,"假設(shè)我們創(chuàng)建一個(gè)學(xué)生對(duì)象 ????:"

// 創(chuàng)建一個(gè)學(xué)生對(duì)象
Student student;  

// 在內(nèi)存中占用 32 字節(jié)
// 因?yàn)橛?4 個(gè)指針,每個(gè)指針 8 字節(jié)
// 4 × 8 = 32 字節(jié) ??

"原本一個(gè) int 只需要 4 字節(jié) ??,現(xiàn)在卻要用 8 字節(jié)的指針去指向它。就像點(diǎn)個(gè)炒青菜 ??,還得配個(gè)專職導(dǎo)游 ????!" 老張搖頭晃腦地說(shuō) ??

"而且啊," 老張拿起筆又在白板上寫道 ??,"你想想訪問(wèn)成員時(shí)會(huì)有多麻煩 ??:"

// 簡(jiǎn)單對(duì)象模型下訪問(wèn)成員的復(fù)雜過(guò)程 ??
void useStudent(Student* s) {
    // 第一步:定位 score 指針的地址 ??
    void** score_ptr_addr = (void**)((char*)s + sizeof(void*));
    
    // 第二步:通過(guò)指針找到實(shí)際分?jǐn)?shù) ??
    int* real_score = (int*)(*score_ptr_addr);
    
    // 第三步:終于可以修改分?jǐn)?shù)了 ??
    *real_score = 100;

    // 調(diào)用方法更復(fù)雜 ??
    // 第一步:找到函數(shù)指針的地址 ??
    void** study_ptr_addr = (void**)((char*)s + 2 * sizeof(void*));
    
    // 第二步:獲取實(shí)際的函數(shù)指針 ??
    void (*study_func)(Student*) = 
        (void (*)(Student*))*study_ptr_addr;
    
    // 第三步:調(diào)用函數(shù) ??
    study_func(s);
}

"天哪!" 小王驚呼 ??,"就改個(gè)分?jǐn)?shù)要這么多步驟?"

"是??!" 老張點(diǎn)點(diǎn)頭 ??,"現(xiàn)在我們寫student.score = 100 或student.study() 這么簡(jiǎn)單 ?,但在簡(jiǎn)單對(duì)象模型下,編譯器要做的工作可復(fù)雜了 ??。每次訪問(wèn)成員都要進(jìn)行兩次內(nèi)存尋址:一次找到指針 ??,一次通過(guò)指針找到實(shí)際數(shù)據(jù) ??。"

"而且這還不是全部問(wèn)題 ??," 老張繼續(xù)說(shuō),"想象一下,如果要實(shí)現(xiàn)虛函數(shù),還得再加一層間接尋址。性能損失就更大了 ??。"

"那后來(lái)呢?" 小王來(lái)了興趣 ??

"后來(lái)當(dāng)然是改進(jìn)啦!這就像餐廳最后想通了 ??:'與其把菜放在各個(gè)地方,還不如直接送到客人桌上呢!'" 老張眨眨眼 ??,"這就是我們現(xiàn)在用的內(nèi)存模型,數(shù)據(jù)直接存在對(duì)象里,該多大就多大。"

"不過(guò)呢," 老張神秘地補(bǔ)充道 ??,"這個(gè)看似笨拙的想法,后來(lái)卻啟發(fā)了'成員指針'的設(shè)計(jì)。這又是另一個(gè)有趣的故事了..." ?

小王托著下巴 ??:"老張,您這故事講得,把我都聽餓了..." ?? ??

"哈哈,那正好!" 老張站起身 ??,"我請(qǐng)你吃飯 ??,路上再給你講講成員指針的故事!" ???

表格驅(qū)動(dòng)對(duì)象模型

"吃飽了吧?" 老張笑瞇瞇地問(wèn)道 ??,"現(xiàn)在讓我們繼續(xù)講講 C++ 對(duì)象模型的演進(jìn)。" ??

小王滿足地點(diǎn)點(diǎn)頭 ??:"剛才您說(shuō)到簡(jiǎn)單對(duì)象模型的缺點(diǎn),后來(lái)是怎么改進(jìn)的呢?" ??

"在簡(jiǎn)單對(duì)象模型之后,出現(xiàn)了一個(gè)叫'表格驅(qū)動(dòng)對(duì)象模型'的設(shè)計(jì)。" 老張興致勃勃地在白板上畫起了新的示意圖 ??:

// 學(xué)生類的原始定義 ????
class Student {
    std::string name;    // 學(xué)生姓名
    int score;          // 學(xué)生分?jǐn)?shù)
    void study();      // 學(xué)習(xí)方法
    void doHomework(); // 做作業(yè)方法
};

// ===== 表格驅(qū)動(dòng)模型的內(nèi)部實(shí)現(xiàn) =====

// 數(shù)據(jù)部分:專門存儲(chǔ)成員變量 ??
struct Student_Data {
    std::string name;  // 直接存儲(chǔ)姓名
    int score;        // 直接存儲(chǔ)分?jǐn)?shù)
};

// 表格部分:管理數(shù)據(jù)和函數(shù) ??
struct Student_Table {
    // 指向?qū)嶋H數(shù)據(jù)的指針
    Student_Data* data;     
    
    // 指向函數(shù)表的指針(存儲(chǔ)所有成員函數(shù))
    void** function_table;   
};

"看出區(qū)別了嗎?" 老張指著圖說(shuō) ??,"這個(gè)模型把數(shù)據(jù)和函數(shù)分開存儲(chǔ)。數(shù)據(jù)直接存在對(duì)象里,函數(shù)則通過(guò)一個(gè)表來(lái)管理。" ???

"這樣做有什么好處呢?" 小王充滿好奇地問(wèn)道 ??

"首先,數(shù)據(jù)訪問(wèn)變快了!" 老張興奮地解釋道 ??,"因?yàn)閿?shù)據(jù)直接存儲(chǔ),不用再通過(guò)指針間接訪問(wèn)。其次,這種設(shè)計(jì)為后來(lái)的虛函數(shù)表鋪平了道路。" ?

"啊,原來(lái)虛函數(shù)表是從這里來(lái)的!" 小王恍然大悟 ??

"沒(méi)錯(cuò)," 老張欣慰地點(diǎn)頭 ??,"這就是為什么我們說(shuō)要理解 C++ 的歷史演進(jìn)。每個(gè)設(shè)計(jì)都不是憑空出現(xiàn)的,都是在解決實(shí)際問(wèn)題中逐步優(yōu)化的結(jié)果。" ??

"不過(guò)啊," 老張喝了口咖啡繼續(xù)說(shuō) ??,"表格驅(qū)動(dòng)模型雖然比簡(jiǎn)單對(duì)象模型好,但還是存在一些問(wèn)題。" ??

小王認(rèn)真地聽著 ??

"讓我給你列舉幾個(gè)主要問(wèn)題:" 老張掰著手指數(shù)道 ??

// 問(wèn)題演示 1: 內(nèi)存占用問(wèn)題 ??
struct Student_Table {
    Student_Data* data;      // 8字節(jié)
    void** function_table;   // 8字節(jié)
};  // 總共需要 16 字節(jié)!

// 問(wèn)題演示 2: 訪問(wèn)效率問(wèn)題 ??
void example() {
    Student s;
    
    // 訪問(wèn)數(shù)據(jù)要兩次解引用 
    int score = s.table->data->score;  
    
    // 調(diào)用函數(shù)更復(fù)雜
    void (*func)() = s.table->function_table[0];
    func();
}

"你看," 老張指著代碼說(shuō) ??,"首先是內(nèi)存問(wèn)題。即使一個(gè)空類,也要占用至少 16 個(gè)字節(jié)來(lái)存儲(chǔ)兩個(gè)指針!" ??

"其次是性能問(wèn)題," 老張繼續(xù)解釋 ??,"每次訪問(wèn)數(shù)據(jù)都要經(jīng)過(guò)兩次指針跳轉(zhuǎn),調(diào)用函數(shù)更是要先找表,再找函數(shù)..."

"這不就和簡(jiǎn)單對(duì)象模型一樣慢嗎?" 小王皺眉道 ??

"沒(méi)錯(cuò)!" 老張點(diǎn)點(diǎn)頭 ??,"所以后來(lái)就有了現(xiàn)代 C++ 對(duì)象模型,它采用了一種更聰明的方式。" ?

"那現(xiàn)代的對(duì)象模型又是什么樣的呢?" 小王繼續(xù)追問(wèn) ??

"這個(gè)嘛..." 老張神秘地笑了笑 ??,"我們下次再講。" ?? ?

現(xiàn)代 C++ 對(duì)象模型

"說(shuō)到現(xiàn)代 C++ 對(duì)象模型," 老張站起來(lái)走到白板前,"這可是 Stroustrup 大師經(jīng)過(guò)反復(fù)權(quán)衡后的杰作。"

"它有什么特別之處呢?" 小王問(wèn)道。

"我們用一個(gè)經(jīng)典的例子來(lái)說(shuō)明。" 老張?jiān)诎装迳蠈懙?

// 現(xiàn)代 C++ 對(duì)象模型示例 ??
class ModernStudent {
    // 數(shù)據(jù)直接存儲(chǔ)在對(duì)象中 ??
    std::string name;   
    int score;         
    
    // 普通函數(shù)直接編譯為獨(dú)立函數(shù) ??
    void study() { 
        // 直接訪問(wèn)數(shù)據(jù)成員
        score += 10;  
    }
    
    // 虛函數(shù)才使用虛表 ??
    virtual void doHomework() {
        // 通過(guò)虛表調(diào)用
    }
    
private:
    // 只有需要多態(tài)的類才有虛表指針
    void* vptr;  // 虛函數(shù)表指針 ??
};

"現(xiàn)代對(duì)象模型的特點(diǎn)是:" 老張總結(jié)道 ??

  • 數(shù)據(jù)成員直接存儲(chǔ) ??
  • 普通成員函數(shù)獨(dú)立存儲(chǔ) ??
  • 只在需要多態(tài)時(shí)才使用虛表 ??
  • 最大限度減少間接訪問(wèn) ??

"這樣既保證了性能,又支持了 C++ 的所有特性!" 老張笑著說(shuō) ??

"原來(lái)是這樣!" 小王恍然大悟 ??,"那這就解釋了為什么有些類比其他類占用內(nèi)存更多 - 因?yàn)樗鼈冃枰摫碇羔?"

"聰明!" 老張贊許地點(diǎn)點(diǎn)頭 ??,"這就是為什么我們說(shuō) - 要理解 C++ 的性能特性,必須先理解它的對(duì)象模型。" ??

"那虛表具體是怎么工作的呢?" 小王繼續(xù)追問(wèn) ??

"這個(gè)問(wèn)題問(wèn)得好!" 老張眼睛一亮 ?,"不過(guò)這是另一個(gè)精彩的話題了,我們下次再聊..."

小王若有所思地點(diǎn)點(diǎn)頭 ??,"感覺(jué) C++ 每個(gè)特性背后都有這么多故事啊!"

"是啊," 老張笑道 ??,"這就是 C++ 的魅力所在。它的每個(gè)設(shè)計(jì)決策,都是在實(shí)踐中不斷優(yōu)化的結(jié)果。" ??

虛函數(shù)表工作原理

"說(shuō)到虛函數(shù)表啊..." 老張放下咖啡杯,眼睛里閃著光 ?,"這可是 C++ 里最精妙的設(shè)計(jì)之一。"

"為什么這么說(shuō)?" 小王好奇地問(wèn)道 ??

"想啊," 老張拿起馬克筆走向白板,"C++ 需要在運(yùn)行時(shí)才能確定調(diào)用哪個(gè)函數(shù),但又要保證性能不受太大影響。這就像餐廳里的點(diǎn)菜系統(tǒng),既要讓客人能隨時(shí)換菜,又不能讓服務(wù)員跑來(lái)跑去問(wèn)廚師 - 這可怎么辦呢?" ??♂?

"??!就像餐廳的電子菜單?" 小王眼前一亮 ??

"沒(méi)錯(cuò)! ??" 老張開心地笑著說(shuō) ??, "虛函數(shù)表就像是每個(gè)類的'專屬菜單' ??。來(lái),讓我給你畫個(gè)生動(dòng)的例子..." ?

// 動(dòng)物基類 ??
class Animal {
public:
    // 構(gòu)造函數(shù),初始化動(dòng)物名字 ???
    Animal(conststd::string& name) : _name(name) {}
    
    // 虛析構(gòu)函數(shù),確保正確釋放內(nèi)存 ???
    virtual ~Animal() = default;
    
    // 純虛函數(shù),所有動(dòng)物都要實(shí)現(xiàn)發(fā)聲 ??
    virtual void makeSound() = 0;  
    
protected:
    std::string _name;    // 動(dòng)物的名字 ??
    
private:
    staticint population;  // 所有動(dòng)物的數(shù)量統(tǒng)計(jì) ??
};

// 貓咪類 - 繼承自動(dòng)物 ??
class Cat :public Animal {
public:
    // 構(gòu)造小貓咪 ??
    Cat(conststd::string& name) : Animal(name) {}
    
    // 實(shí)現(xiàn)貓咪的叫聲 ??
    virtual void makeSound() override { 
        std::cout << "喵喵喵~" << std::endl; 
    }
    
private:
    int _lives = 9;  // 貓有9條命 ?
};

"在現(xiàn)代 C++ 中 ??," 老張拿起馬克筆繼續(xù)畫圖 ??, "這些類在內(nèi)存中的布局是這樣的:"

// Animal 在內(nèi)存中的實(shí)際布局 ??
struct Animal_Layout {
    void* vptr;         // 虛函數(shù)表指針 ??
    std::string _name;  // 名字成員變量 ??
};

// Cat 在內(nèi)存中的實(shí)際布局 ??
struct Cat_Layout {
    Animal_Layout base;  // 繼承的基類部分 ??
    int _lives;         // Cat獨(dú)有的成員 ??
};

// 類外部的靜態(tài)成員 ??
static int Animal::population;  // 存儲(chǔ)在數(shù)據(jù)段中

"這個(gè)設(shè)計(jì)展示了幾個(gè)超級(jí)重要的特點(diǎn) ??:" 老張指著白板興奮地說(shuō) ??:

  • 虛表指針總是在最前面 ??
  • 基類成員排在前面 ??
  • 派生類成員在后面 ??
  • 虛函數(shù)通過(guò)表格查找調(diào)用 ??

"這樣的設(shè)計(jì)既高效又靈活 ??,是 C++ 智慧的結(jié)晶呢!" 老張總結(jié)道 ?

如何訪問(wèn)虛函數(shù)表? 

"讓我們一步步看看虛函數(shù)調(diào)用背后發(fā)生了什么," 老張拿起馬克筆 ??, "首先創(chuàng)建一個(gè)貓咪對(duì)象:"

Cat kitty("咪咪");  // 創(chuàng)建貓咪對(duì)象

"當(dāng)我們創(chuàng)建這個(gè)對(duì)象時(shí)," 老張解釋道 ????, "編譯器會(huì)自動(dòng)初始化虛表指針(vptr),指向 Cat 類的虛函數(shù)表。"

"接下來(lái),當(dāng)我們調(diào)用虛函數(shù)時(shí):"

kitty.makeSound();  // 看起來(lái)很簡(jiǎn)單的一行代碼

"但在底層,編譯器會(huì)生成一系列復(fù)雜的操作。首先是獲取虛表指針:"

// 第一步:獲取對(duì)象的虛表指針
void** vptr = *(void***)(&kitty);  // 從對(duì)象內(nèi)存布局的開始位置讀取虛表指針

// 指針層次分析:
// === 第1步:獲取對(duì)象地址 ===
Cat* cat_ptr = &kitty;
// cat_ptr 現(xiàn)在指向?qū)ο蟮钠鹗嘉恢?// 因?yàn)樘摫碇羔樋偸窃趯?duì)象的最開始位置,所以這個(gè)地址
// 實(shí)際上就指向了虛表指針的存儲(chǔ)位置

// === 第2步:轉(zhuǎn)換為 void*** 類型 ===
void*** triple_ptr = (void***)cat_ptr;
// 為什么要轉(zhuǎn)換成 void***?
// - 因?yàn)槲覀円ㄟ^(guò)這個(gè)指針去讀取虛表指針
// - 虛表指針本身的類型是 void**
// - 所以指向虛表指針的指針就是 void***

// === 第3步:解引用獲取虛表指針 ===
void** vptr = *triple_ptr;
// 現(xiàn)在 vptr 就是真正的虛表指針了
// - 它指向了函數(shù)指針數(shù)組(虛函數(shù)表)
// - 類型是 void**,因?yàn)樗赶虻氖呛瘮?shù)指針數(shù)組

// 內(nèi)存布局示意:
/*
內(nèi)存地址     內(nèi)容                  類型
0x1000   [ 虛表指針 ]            void**    <-- cat_ptr/triple_ptr 指向這里
         [ name成員 ]            string
         [ lives成員 ]           int

0x2000   [ 析構(gòu)函數(shù)指針 ]        void*     <-- vptr 指向這里(虛函數(shù)表的開始)
         [ makeSound指針 ]       void*
         [ eat指針 ]            void*
         [ purr指針 ]           void*
*/

"讓我們?cè)敿?xì)解釋一下這個(gè)指針轉(zhuǎn)換過(guò)程:" 老張拿起馬克筆畫起示意圖 ??


// 假設(shè)有一個(gè) Cat 對(duì)象
Cat kitty("咪咪");

// 獲取虛表指針的詳細(xì)步驟分解
void** vptr = *(void***)(&kitty);

/* 讓我們一步步解析這行代碼:

1. &kitty 得到 Cat* 類型
   - 這是對(duì)象的起始地址
   - 因?yàn)樘摫碇羔樤趯?duì)象的開頭,所以這個(gè)地址就是虛表指針的位置

2. (void***) 轉(zhuǎn)換
   - 為什么需要 void***?
   - 因?yàn)槲覀円?
     a) 首先通過(guò)指針訪問(wèn)對(duì)象 (第一個(gè)*)
     b) 對(duì)象開頭存儲(chǔ)的是虛表指針 (第二個(gè)*)
     c) 虛表本身是函數(shù)指針數(shù)組 (第三個(gè)*)
   - 這就像一個(gè)三層的包裝盒:
     最外層: 對(duì)象地址
     中間層: 虛表指針
     最內(nèi)層: 函數(shù)指針數(shù)組

3. *(void***) 解引用
   - 這一步實(shí)際獲取了虛表指針
   - 結(jié)果類型是 void**,正好是函數(shù)指針數(shù)組的類型
*/

"這樣理解起來(lái)容易多了吧?" 老張問(wèn)道 ??

"哦~" 小王恍然大悟 ??,"原來(lái)這些指針操作是為了層層剝開對(duì)象的結(jié)構(gòu),最終找到虛函數(shù)表!"

"這里用了兩次指針轉(zhuǎn)換," 老張指著代碼說(shuō) ??, "&kitty 得到對(duì)象地址,然后通過(guò)指針轉(zhuǎn)換和解引用,找到虛表指針。"

"讓我用一個(gè)三維數(shù)組的類比來(lái)幫大家更好地理解這個(gè)指針結(jié)構(gòu)," 老張補(bǔ)充道 ????

// 想象一個(gè)三維數(shù)組結(jié)構(gòu)
Class[N][M][K] objects;

/*
三個(gè)維度分別代表:
第一維 [N]: 不同的類
    - 每個(gè)類都有自己的虛函數(shù)表
    - 比如 Animal、Cat、Dog 等類

第二維 [M]: 虛函數(shù)表
    - 存儲(chǔ)了該類所有的虛函數(shù)指針
    - 包括繼承的、覆寫的和新增的函數(shù)

第三維 [K]: 具體的函數(shù)指針
    - 指向?qū)嶋H的函數(shù)實(shí)現(xiàn)
    - 比如 makeSound、eat 等方法

訪問(wèn)過(guò)程就像在這個(gè)三維空間中導(dǎo)航:
1. 通過(guò)對(duì)象找到對(duì)應(yīng)的類 (第一維)
2. 獲取該類的虛函數(shù)表 (第二維)
3. 在表中找到具體的函數(shù)指針 (第三維)
*/

// 用代碼表示這個(gè)訪問(wèn)過(guò)程
void callVirtualFunction(Cat* obj, int funcIndex) {
    // 第一維:通過(guò)對(duì)象找到類的虛表指針
    void*** classPtr = (void***)obj;
    
    // 第二維:獲取虛函數(shù)表
    void** vtable = *classPtr;
    
    // 第三維:獲取具體函數(shù)指針
    void* funcPtr = vtable[funcIndex];
    
    // 調(diào)用函數(shù)
    ((void(*)(Cat*))funcPtr)(obj);
}

"看到了嗎?" 老張指著圖說(shuō) ?? "就像在一個(gè)三維空間中導(dǎo)航:"

  • "第一維就像一個(gè)類的博物館 ???,每個(gè)展廳都是一個(gè)不同的類"
  • "第二維就像每個(gè)展廳里的展示柜 ??,里面陳列著該類的所有虛函數(shù)"
  • "第三維就是展示柜中的具體展品 ??,也就是實(shí)際的函數(shù)實(shí)現(xiàn)"

"當(dāng)我們通過(guò)對(duì)象調(diào)用虛函數(shù)時(shí),就像是在這個(gè)三維空間中找到正確的'展品'。" 老張解釋道 ???

"啊!這么一說(shuō)就清楚多了!" 小王眼睛一亮 ?? "每次解引用就是在不同維度間穿梭!"

如何通過(guò)虛函數(shù)表獲取函數(shù)地址?

"接著是在虛表中查找函數(shù)地址:"

// 第二步:在虛表中查找函數(shù)地址
void (*makeSound)(Cat*) = vptr[1];  // 虛表中查找 makeSound 函數(shù)指針

"虛表就像一個(gè)函數(shù)指針數(shù)組," 老張繼續(xù)解釋 ??, "每個(gè)虛函數(shù)在表中都有固定的位置。這里的 [1] 表示 makeSound 在虛表中的偏移位置。"

"等等," 小王突然舉手問(wèn)道 ??, "這個(gè) [1] 是怎么來(lái)的?為什么是 1 而不是其他數(shù)字?"

"?。?wèn)得好!" 老張笑著說(shuō) ??, "虛表中的函數(shù)位置是在編譯時(shí)就確定好的。讓我詳細(xì)解釋一下..."

老張?jiān)诎装迳袭嬈鹆诵碌氖疽鈭D:

// Animal 類的虛函數(shù)表布局 ??
struct Animal_VTable {
    // [0] 析構(gòu)函數(shù)永遠(yuǎn)在第一個(gè)位置
    void (*destructor)(Animal*);     
    // [1] makeSound 在第二個(gè)位置
    void (*makeSound)(Animal*);      
    // 如果還有其他虛函數(shù),繼續(xù)往后排...
};

// Cat 類繼承并覆寫了這些函數(shù)
struct Cat_VTable {
    // [0] Cat 的析構(gòu)函數(shù)
    void (*destructor)(Cat*);        
    // [1] Cat 的 makeSound 實(shí)現(xiàn)
    void (*makeSound)(Cat*);         
};

"編譯器遵循以下規(guī)則來(lái)安排虛函數(shù)的位置 ??:" 老張解釋道:

  • "虛析構(gòu)函數(shù)總是位于索引 [0] 的位置 ??"
  • "其他虛函數(shù)按照它們?cè)诨愔惺状温暶鞯捻樞蚺帕???"
  • "派生類如果覆寫了基類的虛函數(shù),就使用相同的位置 ??"
  • "派生類新增的虛函數(shù)放在表的末尾 ??"

"比如說(shuō),如果我們擴(kuò)展一下這個(gè)例子:" 老張繼續(xù)寫道:

class Animal {
public:
    virtual ~Animal();                // 位置 [0]
    virtual void makeSound() = 0;     // 位置 [1]
    virtual void eat();               // 位置 [2]
};

class Cat : public Animal {
public:
    virtual ~Cat();                   // 位置 [0]
    virtual void makeSound() override;// 位置 [1]
    virtual void eat() override;      // 位置 [2]
    virtual void purr();              // 位置 [3] - Cat特有
};

"所以當(dāng)我們調(diào)用kitty.makeSound() 時(shí),編譯器知道 makeSound 在位置 [1],這是在編譯時(shí)就確定好的,運(yùn)行時(shí)直接用這個(gè)固定位置去查找,非常高效!" 老張總結(jié)道 ??

"原來(lái)如此!" 小王恍然大悟 ??, "這就像圖書館的分類系統(tǒng),每本書都有固定的位置編號(hào)!"

"沒(méi)錯(cuò)!" 老張點(diǎn)頭贊許 ??, "而且這種設(shè)計(jì)還保證了即使基類添加了新的虛函數(shù),已有的函數(shù)位置也不會(huì)改變,這對(duì)二進(jìn)制兼容性非常重要。"

"最后,才是實(shí)際調(diào)用函數(shù):"

// 第三步:調(diào)用找到的函數(shù)
makeSound(&kitty);  // 傳入 this 指針調(diào)用函數(shù)

"看到了嗎?" 老張總結(jié)道 ??, "一個(gè)簡(jiǎn)單的虛函數(shù)調(diào)用,背后其實(shí)包含了三個(gè)關(guān)鍵步驟:

  • 獲取虛表指針
  • 查找函數(shù)地址
  • 調(diào)用目標(biāo)函數(shù)

這就是為什么虛函數(shù)調(diào)用會(huì)比普通函數(shù)調(diào)用慢一點(diǎn) - 它需要額外的間接尋址操作。"

小王恍然大悟 ??: "原來(lái)如此!這就解釋了為什么有些性能敏感的代碼會(huì)避免使用虛函數(shù)。"

"沒(méi)錯(cuò)!" 老張點(diǎn)點(diǎn)頭 ??, "不過(guò)現(xiàn)代 CPU 的分支預(yù)測(cè)已經(jīng)很強(qiáng)大了,所以除非在特別關(guān)鍵的性能熱點(diǎn),否則虛函數(shù)的開銷通常不會(huì)造成明顯影響。"

"這比之前的簡(jiǎn)單對(duì)象模型和表格驅(qū)動(dòng)模型高明多了!" 小王驚嘆道。

"是的," 老張點(diǎn)頭,"這個(gè)設(shè)計(jì)既保證了性能,又支持了多態(tài)。"

同類對(duì)象的虛函數(shù)表共享機(jī)制

"對(duì)了老張!" 小王突然想到一個(gè)問(wèn)題 ??,"每個(gè)對(duì)象都有一個(gè)虛表指針,那虛函數(shù)表本身是每個(gè)對(duì)象都有一份嗎?"

老張笑著搖搖頭 ??:"這個(gè)問(wèn)題問(wèn)得好!虛函數(shù)表是由編譯器為每個(gè)類創(chuàng)建的,而不是每個(gè)對(duì)象。所有同類型的對(duì)象共享同一個(gè)虛函數(shù)表!"

"讓我畫個(gè)圖解釋一下:" 老張走向白板 ??:

// 內(nèi)存布局示意圖 ??

// 代碼段(只讀)中存儲(chǔ)的虛函數(shù)表
const Cat_VTable {              // ?? 所有 Cat 對(duì)象共享這個(gè)表
    &Cat::destructor,          // [0]
    &Cat::makeSound,          // [1]
    &Cat::eat,               // [2]
    &Cat::purr              // [3]
};

// 堆/棧中的對(duì)象
Cat cat1("咪咪");   // ?? 對(duì)象1
// {
//     vptr -> Cat_VTable    // 指向共享的虛函數(shù)表
//     name: "咪咪"
//     lives: 9
// }

Cat cat2("花花");   // ?? 對(duì)象2
// {
//     vptr -> Cat_VTable    // 指向相同的虛函數(shù)表
//     name: "花花"
//     lives: 9
// }

"你看," 老張指著圖解釋道 ??,"虛函數(shù)表存儲(chǔ)在程序的只讀數(shù)據(jù)段(.rodata)中,是只讀的。每個(gè) Cat 對(duì)象的 vptr 都指向這同一個(gè)表。這樣設(shè)計(jì)有幾個(gè)重要好處:"

  • "節(jié)省內(nèi)存 ?? - 不需要為每個(gè)對(duì)象都存儲(chǔ)一份完整的函數(shù)表"
  • "提高緩存效率 ?? - 因?yàn)樗袑?duì)象共享同一份表,增加了緩存命中率"
  • "保證一致性 ? - 所有對(duì)象調(diào)用的都是同一份虛函數(shù)實(shí)現(xiàn)"

"哇!這設(shè)計(jì)真是太巧妙了!" 小王贊嘆道 ??,"那是不是說(shuō),一個(gè)程序里面,每個(gè)類只會(huì)有一份虛函數(shù)表?"

"基本上是這樣。" 老張點(diǎn)點(diǎn)頭 ??,"不過(guò)要注意,如果你的程序使用了動(dòng)態(tài)庫(kù),同一個(gè)類可能在不同的動(dòng)態(tài)庫(kù)中各有一份虛函數(shù)表。但在同一個(gè)編譯單元內(nèi),確實(shí)是共享同一份表的。"

"這就像一個(gè)大餐廳的菜單系統(tǒng)," 老張舉例說(shuō) ??,"每個(gè)服務(wù)員(對(duì)象)手里都有一個(gè)平板(vptr),但他們都連接到同一個(gè)中央點(diǎn)餐系統(tǒng)(虛函數(shù)表)。這樣不管哪個(gè)服務(wù)員接單,都能保證點(diǎn)到一樣的菜!" ???

小王若有所思地點(diǎn)點(diǎn)頭 ??:"所以虛函數(shù)的內(nèi)存開銷主要是每個(gè)對(duì)象都要多存一個(gè)指針,而不是虛函數(shù)表本身?"

"聰明!" 老張贊許地說(shuō) ??,"這就是為什么在 C++ 中,一個(gè)帶有虛函數(shù)的類的對(duì)象,至少要比沒(méi)有虛函數(shù)的類多占用一個(gè)指針的大小。在 64 位系統(tǒng)上就是 8 字節(jié)。"

派生類和基類的虛函數(shù)表關(guān)系

"等等," 小王突然想到什么 ??, "那派生類和基類的虛函數(shù)表是怎么回事?它們也是共享一個(gè)表嗎?"

"啊,這個(gè)問(wèn)題問(wèn)得好!" 老張眼睛一亮 ?, "派生類會(huì)有自己獨(dú)立的虛函數(shù)表,而不是和基類共享。讓我畫個(gè)圖解釋一下:"

// 基類 Animal 的虛函數(shù)表
const Animal_VTable {           // ?? 基類表
    &Animal::destructor,       // [0]
    &Animal::makeSound,       // [1]
    &Animal::eat             // [2]
};

// 派生類 Cat 的虛函數(shù)表
const Cat_VTable {             // ?? 派生類有自己的表
    &Cat::destructor,         // [0] 覆寫的析構(gòu)函數(shù)
    &Cat::makeSound,         // [1] 覆寫的 makeSound
    &Cat::eat,              // [2] 覆寫的 eat
    &Cat::purr             // [3] Cat 特有的函數(shù)
};

// 內(nèi)存中的對(duì)象
Animal* p1 = new Animal();  // 基類對(duì)象
// {
//     vptr -> Animal_VTable   // 指向 Animal 的表
//     name: "動(dòng)物"
// }

Animal* p2 = new Cat();     // 通過(guò)基類指針指向派生類對(duì)象
// {
//     vptr -> Cat_VTable      // 指向 Cat 的表!
//     name: "咪咪"
//     lives: 9
// }

"看到了嗎?" 老張指著圖說(shuō) ??, "每個(gè)類都有自己的虛函數(shù)表,這樣做有幾個(gè)重要原因:"

  • "多態(tài)的實(shí)現(xiàn) ?? - 當(dāng)通過(guò)基類指針調(diào)用虛函數(shù)時(shí),實(shí)際會(huì)根據(jù)對(duì)象的真實(shí)類型找到正確的函數(shù)版本"
  • "函數(shù)覆寫的支持 ?? - 派生類可以替換掉繼承來(lái)的虛函數(shù)實(shí)現(xiàn)"
  • "擴(kuò)展的靈活性 ?? - 派生類可以添加新的虛函數(shù)"

"所以說(shuō)," 老張繼續(xù)解釋道 ????, "雖然每個(gè)類型都有自己的虛函數(shù)表,但同一個(gè)類型的所有對(duì)象還是共享同一個(gè)表。這就是 C++ 多態(tài)的精妙之處!"

"原來(lái)如此!" 小王恍然大悟 ??, "這就像每個(gè)餐廳分店(類)都有自己的菜單(虛函數(shù)表),但同一個(gè)分店的所有服務(wù)員(對(duì)象)都用同一份菜單!"

"沒(méi)錯(cuò)!" 老張笑著說(shuō) ??, "而且你注意到了嗎?即使用基類指針指向派生類對(duì)象,對(duì)象的 vptr 也是指向派生類的虛函數(shù)表。這就是為什么我們能通過(guò)基類指針正確調(diào)用派生類的函數(shù)實(shí)現(xiàn)!"

"這設(shè)計(jì)真是太巧妙了!" 小王贊嘆道 ??, "每個(gè)類一份虛函數(shù)表,每個(gè)對(duì)象一個(gè)指針,就實(shí)現(xiàn)了如此強(qiáng)大的多態(tài)機(jī)制!"

老張喝了口咖啡,笑著說(shuō)道 ??: "今天講的這些只是虛函數(shù)表的基礎(chǔ)知識(shí)。要完全理解 C++ 的對(duì)象模型,還有很多有趣的話題要探討呢!"

"比如說(shuō)?" 小王來(lái)了興趣 ??

"比如..." 老張神秘地眨眨眼 ??:

  • 虛函數(shù)表是在什么時(shí)候、怎么創(chuàng)建的? ???
  • 多重繼承時(shí)的虛函數(shù)表是什么樣的? ??
  • 虛繼承又會(huì)帶來(lái)哪些特殊的內(nèi)存布局? ??
  • 構(gòu)造和析構(gòu)過(guò)程中的虛函數(shù)調(diào)用又是怎么處理的? ?

"這些都是非常有趣的話題," 老張站起身來(lái) ??♂?, "不過(guò)這些精彩的內(nèi)容,我們下次再聊..."

小王若有所思地點(diǎn)點(diǎn)頭 ??, "感覺(jué) C++ 的每個(gè)特性背后都藏著這么多精妙的設(shè)計(jì)啊!"

"是啊," 老張笑著說(shuō) ??, "這就是為什么即使到今天,研究 C++ 的底層實(shí)現(xiàn)依然是那么有趣。" 

責(zé)任編輯:趙寧寧 來(lái)源: everystep
相關(guān)推薦

2010-01-18 17:38:54

C++虛函數(shù)表

2010-02-01 11:22:09

C++虛函數(shù)

2022-07-18 15:32:37

C++虛函數(shù)表

2010-01-27 10:36:54

C++虛函數(shù)

2024-01-23 10:13:57

C++虛函數(shù)

2024-04-22 13:22:00

虛函數(shù)象編程C++

2011-05-24 16:20:27

虛函數(shù)

2022-05-09 08:37:43

IO模型Java

2010-01-20 18:06:06

C++虛基類

2020-12-28 08:36:30

C語(yǔ)言編程泛型

2020-06-08 17:35:27

Redis集群互聯(lián)網(wǎng)

2022-12-14 07:32:40

InnoDBMySQL引擎

2022-01-12 19:59:19

Netty 核心啟動(dòng)

2011-05-24 16:30:35

虛函數(shù)

2024-12-19 14:42:15

C++內(nèi)存泄漏內(nèi)存管理

2010-02-05 13:35:19

C++虛析構(gòu)函數(shù)

2009-03-11 14:42:57

面試求職案例

2023-05-08 07:52:29

JSXReactHooks

2021-11-10 09:45:06

Lambda表達(dá)式語(yǔ)言

2011-07-20 17:04:55

C++虛函數(shù)動(dòng)態(tài)聯(lián)編
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)