成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

原來 C++ 虛函數是這樣實現的!

開發
本文讓我們拋開枯燥的概念講解,通過一個有趣的故事,一步步揭開 C++ 對象模型的神秘面紗。

"為什么我的程序這么占內存?" 

"虛函數到底是怎么實現的?" 

"多態背后的原理是什么?" 

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

C++ 對象模型

"誒,小王啊!" 老張端著冒著熱氣的咖啡杯走進辦公室,眼睛里閃著神秘的光 ?? "今天咱們來聊個有意思的歷史故事~"

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

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

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

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

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

老張搖搖手指:"聰明著呢!Stroustrup 大師當年在貝爾實驗室可是深謀遠慮啊。你想啊,C 編譯器都已經很成熟了,何必重復造輪子呢?而且這樣一來,C++ 代碼還能和 C 代碼愉快地玩耍,簡直是一箭雙雕!" ??

"來來來," 老張站起身,走到白板前,"讓我給你變個魔術,看看 C++ 代碼是怎么'變身'的~" ?

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

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

"這段 C++ 代碼經過編譯器處理后,本質上會變成這樣的 C 代碼" 老張微笑著說

// C 語言結構體定義 ??
struct Rectangle {
    int width;   // 存儲寬度
    int height;  // 存儲高度
};

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

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

小王恍然大悟:"原來如此!C++ 的類其實就是在 C 的基礎上做了語法糖啊!"

老張點點頭:"是的!所有的成員函數都會被轉換成普通的 C 函數,只是多了一個 this 指針參數。這就是為什么我們說,要真正理解 C++,必須先理解 C 語言的基礎。" 

"那繼承是怎么實現的呢?" 小王充滿好奇地問道 ??

老張笑著說 ??:"這個問題問得好!讓我們繼續深入了解 C++ 對象模型的奧秘..."

簡單對象模型

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

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

"知道為什么現在的 C++ 類寫起來這么優雅嗎?" 老張神秘地笑著說 ??♂?,"這還得從上古時代說起..."

老張在白板上寫下一段代碼 ????:

// 一個簡單的學生類 ????
class Student {
    std::string name;     // 存儲學生姓名 ??
    int score;           // 記錄考試分數 ??
    void study();       // 學習方法 ??
    void doHomework();  // 寫作業功能 ??
};

"猜猜看,在 C++ 剛誕生的年代,Stroustrup 大師是怎么實現這個類的?" 老張眨眨眼 ??

小王搖搖頭 ??:"難道...和現在不一樣嗎?"

"哈哈,那時候為了簡化編譯器的實現,他們設計了'簡單對象模型'" ??

老張畫出了內部實現示意圖 ??:

// 在內存中的實際表示
struct Student_Internal {
    void* name_ptr;      // 指向實際的 string 數據
    void* score_ptr;     // 指向實際的 int 數據
    void* study_ptr;     // 指向 study 函數
    void* homework_ptr;  // 指向 homework 函數
};

"看到沒?" 老張指著圖說 ??,"所有成員,不管是數據還是函數,統統變成指針!就像一個巨大的導航表 ??? ??"

小王瞪大了眼睛 ??:"等等...那豈不是一個簡單的 int 也要用指針來存?" ??

"沒錯!" 老張笑著說 ??,"想象一下,你點外賣 ??,每個菜都要先送到隔壁小區,然后給你一張地址條 ??,告訴你:'你的紅燒肉在A棟3層 ??,炒青菜在B棟5層...' ?? ??♂?"

"這...這不是很浪費嗎?" 小王忍不住笑了 ??

"所以啊!" 老張喝了口咖啡 ??,"假設我們創建一個學生對象 ????:"

// 創建一個學生對象
Student student;  

// 在內存中占用 32 字節
// 因為有 4 個指針,每個指針 8 字節
// 4 × 8 = 32 字節 ??

"原本一個 int 只需要 4 字節 ??,現在卻要用 8 字節的指針去指向它。就像點個炒青菜 ??,還得配個專職導游 ????!" 老張搖頭晃腦地說 ??

"而且啊," 老張拿起筆又在白板上寫道 ??,"你想想訪問成員時會有多麻煩 ??:"

// 簡單對象模型下訪問成員的復雜過程 ??
void useStudent(Student* s) {
    // 第一步:定位 score 指針的地址 ??
    void** score_ptr_addr = (void**)((char*)s + sizeof(void*));
    
    // 第二步:通過指針找到實際分數 ??
    int* real_score = (int*)(*score_ptr_addr);
    
    // 第三步:終于可以修改分數了 ??
    *real_score = 100;

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

"天哪!" 小王驚呼 ??,"就改個分數要這么多步驟?"

"是啊!" 老張點點頭 ??,"現在我們寫student.score = 100 或student.study() 這么簡單 ?,但在簡單對象模型下,編譯器要做的工作可復雜了 ??。每次訪問成員都要進行兩次內存尋址:一次找到指針 ??,一次通過指針找到實際數據 ??。"

"而且這還不是全部問題 ??," 老張繼續說,"想象一下,如果要實現虛函數,還得再加一層間接尋址。性能損失就更大了 ??。"

"那后來呢?" 小王來了興趣 ??

"后來當然是改進啦!這就像餐廳最后想通了 ??:'與其把菜放在各個地方,還不如直接送到客人桌上呢!'" 老張眨眨眼 ??,"這就是我們現在用的內存模型,數據直接存在對象里,該多大就多大。"

"不過呢," 老張神秘地補充道 ??,"這個看似笨拙的想法,后來卻啟發了'成員指針'的設計。這又是另一個有趣的故事了..." ?

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

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

表格驅動對象模型

"吃飽了吧?" 老張笑瞇瞇地問道 ??,"現在讓我們繼續講講 C++ 對象模型的演進。" ??

小王滿足地點點頭 ??:"剛才您說到簡單對象模型的缺點,后來是怎么改進的呢?" ??

"在簡單對象模型之后,出現了一個叫'表格驅動對象模型'的設計。" 老張興致勃勃地在白板上畫起了新的示意圖 ??:

// 學生類的原始定義 ????
class Student {
    std::string name;    // 學生姓名
    int score;          // 學生分數
    void study();      // 學習方法
    void doHomework(); // 做作業方法
};

// ===== 表格驅動模型的內部實現 =====

// 數據部分:專門存儲成員變量 ??
struct Student_Data {
    std::string name;  // 直接存儲姓名
    int score;        // 直接存儲分數
};

// 表格部分:管理數據和函數 ??
struct Student_Table {
    // 指向實際數據的指針
    Student_Data* data;     
    
    // 指向函數表的指針(存儲所有成員函數)
    void** function_table;   
};

"看出區別了嗎?" 老張指著圖說 ??,"這個模型把數據和函數分開存儲。數據直接存在對象里,函數則通過一個表來管理。" ???

"這樣做有什么好處呢?" 小王充滿好奇地問道 ??

"首先,數據訪問變快了!" 老張興奮地解釋道 ??,"因為數據直接存儲,不用再通過指針間接訪問。其次,這種設計為后來的虛函數表鋪平了道路。" ?

"啊,原來虛函數表是從這里來的!" 小王恍然大悟 ??

"沒錯," 老張欣慰地點頭 ??,"這就是為什么我們說要理解 C++ 的歷史演進。每個設計都不是憑空出現的,都是在解決實際問題中逐步優化的結果。" ??

"不過啊," 老張喝了口咖啡繼續說 ??,"表格驅動模型雖然比簡單對象模型好,但還是存在一些問題。" ??

小王認真地聽著 ??

"讓我給你列舉幾個主要問題:" 老張掰著手指數道 ??

// 問題演示 1: 內存占用問題 ??
struct Student_Table {
    Student_Data* data;      // 8字節
    void** function_table;   // 8字節
};  // 總共需要 16 字節!

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

"你看," 老張指著代碼說 ??,"首先是內存問題。即使一個空類,也要占用至少 16 個字節來存儲兩個指針!" ??

"其次是性能問題," 老張繼續解釋 ??,"每次訪問數據都要經過兩次指針跳轉,調用函數更是要先找表,再找函數..."

"這不就和簡單對象模型一樣慢嗎?" 小王皺眉道 ??

"沒錯!" 老張點點頭 ??,"所以后來就有了現代 C++ 對象模型,它采用了一種更聰明的方式。" ?

"那現代的對象模型又是什么樣的呢?" 小王繼續追問 ??

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

現代 C++ 對象模型

"說到現代 C++ 對象模型," 老張站起來走到白板前,"這可是 Stroustrup 大師經過反復權衡后的杰作。"

"它有什么特別之處呢?" 小王問道。

"我們用一個經典的例子來說明。" 老張在白板上寫道:

// 現代 C++ 對象模型示例 ??
class ModernStudent {
    // 數據直接存儲在對象中 ??
    std::string name;   
    int score;         
    
    // 普通函數直接編譯為獨立函數 ??
    void study() { 
        // 直接訪問數據成員
        score += 10;  
    }
    
    // 虛函數才使用虛表 ??
    virtual void doHomework() {
        // 通過虛表調用
    }
    
private:
    // 只有需要多態的類才有虛表指針
    void* vptr;  // 虛函數表指針 ??
};

"現代對象模型的特點是:" 老張總結道 ??

  • 數據成員直接存儲 ??
  • 普通成員函數獨立存儲 ??
  • 只在需要多態時才使用虛表 ??
  • 最大限度減少間接訪問 ??

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

"原來是這樣!" 小王恍然大悟 ??,"那這就解釋了為什么有些類比其他類占用內存更多 - 因為它們需要虛表指針!"

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

"那虛表具體是怎么工作的呢?" 小王繼續追問 ??

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

小王若有所思地點點頭 ??,"感覺 C++ 每個特性背后都有這么多故事啊!"

"是啊," 老張笑道 ??,"這就是 C++ 的魅力所在。它的每個設計決策,都是在實踐中不斷優化的結果。" ??

虛函數表工作原理

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

"為什么這么說?" 小王好奇地問道 ??

"想啊," 老張拿起馬克筆走向白板,"C++ 需要在運行時才能確定調用哪個函數,但又要保證性能不受太大影響。這就像餐廳里的點菜系統,既要讓客人能隨時換菜,又不能讓服務員跑來跑去問廚師 - 這可怎么辦呢?" ??♂?

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

"沒錯! ??" 老張開心地笑著說 ??, "虛函數表就像是每個類的'專屬菜單' ??。來,讓我給你畫個生動的例子..." ?

// 動物基類 ??
class Animal {
public:
    // 構造函數,初始化動物名字 ???
    Animal(conststd::string& name) : _name(name) {}
    
    // 虛析構函數,確保正確釋放內存 ???
    virtual ~Animal() = default;
    
    // 純虛函數,所有動物都要實現發聲 ??
    virtual void makeSound() = 0;  
    
protected:
    std::string _name;    // 動物的名字 ??
    
private:
    staticint population;  // 所有動物的數量統計 ??
};

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

"在現代 C++ 中 ??," 老張拿起馬克筆繼續畫圖 ??, "這些類在內存中的布局是這樣的:"

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

// Cat 在內存中的實際布局 ??
struct Cat_Layout {
    Animal_Layout base;  // 繼承的基類部分 ??
    int _lives;         // Cat獨有的成員 ??
};

// 類外部的靜態成員 ??
static int Animal::population;  // 存儲在數據段中

"這個設計展示了幾個超級重要的特點 ??:" 老張指著白板興奮地說 ??:

  • 虛表指針總是在最前面 ??
  • 基類成員排在前面 ??
  • 派生類成員在后面 ??
  • 虛函數通過表格查找調用 ??

"這樣的設計既高效又靈活 ??,是 C++ 智慧的結晶呢!" 老張總結道 ?

如何訪問虛函數表? 

"讓我們一步步看看虛函數調用背后發生了什么," 老張拿起馬克筆 ??, "首先創建一個貓咪對象:"

Cat kitty("咪咪");  // 創建貓咪對象

"當我們創建這個對象時," 老張解釋道 ????, "編譯器會自動初始化虛表指針(vptr),指向 Cat 類的虛函數表。"

"接下來,當我們調用虛函數時:"

kitty.makeSound();  // 看起來很簡單的一行代碼

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

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

// 指針層次分析:
// === 第1步:獲取對象地址 ===
Cat* cat_ptr = &kitty;
// cat_ptr 現在指向對象的起始位置
// 因為虛表指針總是在對象的最開始位置,所以這個地址
// 實際上就指向了虛表指針的存儲位置

// === 第2步:轉換為 void*** 類型 ===
void*** triple_ptr = (void***)cat_ptr;
// 為什么要轉換成 void***?
// - 因為我們要通過這個指針去讀取虛表指針
// - 虛表指針本身的類型是 void**
// - 所以指向虛表指針的指針就是 void***

// === 第3步:解引用獲取虛表指針 ===
void** vptr = *triple_ptr;
// 現在 vptr 就是真正的虛表指針了
// - 它指向了函數指針數組(虛函數表)
// - 類型是 void**,因為它指向的是函數指針數組

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

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

"讓我們詳細解釋一下這個指針轉換過程:" 老張拿起馬克筆畫起示意圖 ??


// 假設有一個 Cat 對象
Cat kitty("咪咪");

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

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

1. &kitty 得到 Cat* 類型
   - 這是對象的起始地址
   - 因為虛表指針在對象的開頭,所以這個地址就是虛表指針的位置

2. (void***) 轉換
   - 為什么需要 void***?
   - 因為我們要:
     a) 首先通過指針訪問對象 (第一個*)
     b) 對象開頭存儲的是虛表指針 (第二個*)
     c) 虛表本身是函數指針數組 (第三個*)
   - 這就像一個三層的包裝盒:
     最外層: 對象地址
     中間層: 虛表指針
     最內層: 函數指針數組

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

"這樣理解起來容易多了吧?" 老張問道 ??

"哦~" 小王恍然大悟 ??,"原來這些指針操作是為了層層剝開對象的結構,最終找到虛函數表!"

"這里用了兩次指針轉換," 老張指著代碼說 ??, "&kitty 得到對象地址,然后通過指針轉換和解引用,找到虛表指針。"

"讓我用一個三維數組的類比來幫大家更好地理解這個指針結構," 老張補充道 ????

// 想象一個三維數組結構
Class[N][M][K] objects;

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

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

第三維 [K]: 具體的函數指針
    - 指向實際的函數實現
    - 比如 makeSound、eat 等方法

訪問過程就像在這個三維空間中導航:
1. 通過對象找到對應的類 (第一維)
2. 獲取該類的虛函數表 (第二維)
3. 在表中找到具體的函數指針 (第三維)
*/

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

"看到了嗎?" 老張指著圖說 ?? "就像在一個三維空間中導航:"

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

"當我們通過對象調用虛函數時,就像是在這個三維空間中找到正確的'展品'。" 老張解釋道 ???

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

如何通過虛函數表獲取函數地址?

"接著是在虛表中查找函數地址:"

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

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

"等等," 小王突然舉手問道 ??, "這個 [1] 是怎么來的?為什么是 1 而不是其他數字?"

"啊!問得好!" 老張笑著說 ??, "虛表中的函數位置是在編譯時就確定好的。讓我詳細解釋一下..."

老張在白板上畫起了新的示意圖:

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

// Cat 類繼承并覆寫了這些函數
struct Cat_VTable {
    // [0] Cat 的析構函數
    void (*destructor)(Cat*);        
    // [1] Cat 的 makeSound 實現
    void (*makeSound)(Cat*);         
};

"編譯器遵循以下規則來安排虛函數的位置 ??:" 老張解釋道:

  • "虛析構函數總是位于索引 [0] 的位置 ??"
  • "其他虛函數按照它們在基類中首次聲明的順序排列 ??"
  • "派生類如果覆寫了基類的虛函數,就使用相同的位置 ??"
  • "派生類新增的虛函數放在表的末尾 ??"

"比如說,如果我們擴展一下這個例子:" 老張繼續寫道:

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特有
};

"所以當我們調用kitty.makeSound() 時,編譯器知道 makeSound 在位置 [1],這是在編譯時就確定好的,運行時直接用這個固定位置去查找,非常高效!" 老張總結道 ??

"原來如此!" 小王恍然大悟 ??, "這就像圖書館的分類系統,每本書都有固定的位置編號!"

"沒錯!" 老張點頭贊許 ??, "而且這種設計還保證了即使基類添加了新的虛函數,已有的函數位置也不會改變,這對二進制兼容性非常重要。"

"最后,才是實際調用函數:"

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

"看到了嗎?" 老張總結道 ??, "一個簡單的虛函數調用,背后其實包含了三個關鍵步驟:

  • 獲取虛表指針
  • 查找函數地址
  • 調用目標函數

這就是為什么虛函數調用會比普通函數調用慢一點 - 它需要額外的間接尋址操作。"

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

"沒錯!" 老張點點頭 ??, "不過現代 CPU 的分支預測已經很強大了,所以除非在特別關鍵的性能熱點,否則虛函數的開銷通常不會造成明顯影響。"

"這比之前的簡單對象模型和表格驅動模型高明多了!" 小王驚嘆道。

"是的," 老張點頭,"這個設計既保證了性能,又支持了多態。"

同類對象的虛函數表共享機制

"對了老張!" 小王突然想到一個問題 ??,"每個對象都有一個虛表指針,那虛函數表本身是每個對象都有一份嗎?"

老張笑著搖搖頭 ??:"這個問題問得好!虛函數表是由編譯器為每個類創建的,而不是每個對象。所有同類型的對象共享同一個虛函數表!"

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

// 內存布局示意圖 ??

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

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

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

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

  • "節省內存 ?? - 不需要為每個對象都存儲一份完整的函數表"
  • "提高緩存效率 ?? - 因為所有對象共享同一份表,增加了緩存命中率"
  • "保證一致性 ? - 所有對象調用的都是同一份虛函數實現"

"哇!這設計真是太巧妙了!" 小王贊嘆道 ??,"那是不是說,一個程序里面,每個類只會有一份虛函數表?"

"基本上是這樣。" 老張點點頭 ??,"不過要注意,如果你的程序使用了動態庫,同一個類可能在不同的動態庫中各有一份虛函數表。但在同一個編譯單元內,確實是共享同一份表的。"

"這就像一個大餐廳的菜單系統," 老張舉例說 ??,"每個服務員(對象)手里都有一個平板(vptr),但他們都連接到同一個中央點餐系統(虛函數表)。這樣不管哪個服務員接單,都能保證點到一樣的菜!" ???

小王若有所思地點點頭 ??:"所以虛函數的內存開銷主要是每個對象都要多存一個指針,而不是虛函數表本身?"

"聰明!" 老張贊許地說 ??,"這就是為什么在 C++ 中,一個帶有虛函數的類的對象,至少要比沒有虛函數的類多占用一個指針的大小。在 64 位系統上就是 8 字節。"

派生類和基類的虛函數表關系

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

"啊,這個問題問得好!" 老張眼睛一亮 ?, "派生類會有自己獨立的虛函數表,而不是和基類共享。讓我畫個圖解釋一下:"

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

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

// 內存中的對象
Animal* p1 = new Animal();  // 基類對象
// {
//     vptr -> Animal_VTable   // 指向 Animal 的表
//     name: "動物"
// }

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

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

  • "多態的實現 ?? - 當通過基類指針調用虛函數時,實際會根據對象的真實類型找到正確的函數版本"
  • "函數覆寫的支持 ?? - 派生類可以替換掉繼承來的虛函數實現"
  • "擴展的靈活性 ?? - 派生類可以添加新的虛函數"

"所以說," 老張繼續解釋道 ????, "雖然每個類型都有自己的虛函數表,但同一個類型的所有對象還是共享同一個表。這就是 C++ 多態的精妙之處!"

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

"沒錯!" 老張笑著說 ??, "而且你注意到了嗎?即使用基類指針指向派生類對象,對象的 vptr 也是指向派生類的虛函數表。這就是為什么我們能通過基類指針正確調用派生類的函數實現!"

"這設計真是太巧妙了!" 小王贊嘆道 ??, "每個類一份虛函數表,每個對象一個指針,就實現了如此強大的多態機制!"

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

"比如說?" 小王來了興趣 ??

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

  • 虛函數表是在什么時候、怎么創建的? ???
  • 多重繼承時的虛函數表是什么樣的? ??
  • 虛繼承又會帶來哪些特殊的內存布局? ??
  • 構造和析構過程中的虛函數調用又是怎么處理的? ?

"這些都是非常有趣的話題," 老張站起身來 ??♂?, "不過這些精彩的內容,我們下次再聊..."

小王若有所思地點點頭 ??, "感覺 C++ 的每個特性背后都藏著這么多精妙的設計啊!"

"是啊," 老張笑著說 ??, "這就是為什么即使到今天,研究 C++ 的底層實現依然是那么有趣。" 

責任編輯:趙寧寧 來源: everystep
相關推薦

2010-01-18 17:38:54

C++虛函數表

2010-02-01 11:22:09

C++虛函數

2022-07-18 15:32:37

C++虛函數表

2010-01-27 10:36:54

C++虛函數

2024-04-22 13:22:00

虛函數象編程C++

2024-01-23 10:13:57

C++虛函數

2011-05-24 16:20:27

虛函數

2022-05-09 08:37:43

IO模型Java

2010-01-20 18:06:06

C++虛基類

2020-12-28 08:36:30

C語言編程泛型

2020-06-08 17:35:27

Redis集群互聯網

2022-12-14 07:32:40

InnoDBMySQL引擎

2022-01-12 19:59:19

Netty 核心啟動

2010-02-05 13:35:19

C++虛析構函數

2011-05-24 16:30:35

虛函數

2024-12-19 14:42:15

C++內存泄漏內存管理

2009-03-11 14:42:57

面試求職案例

2023-05-08 07:52:29

JSXReactHooks

2021-11-10 09:45:06

Lambda表達式語言

2011-07-20 17:04:55

C++虛函數動態聯編
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 99亚洲精品视频 | 一级毛片免费完整视频 | 黄在线免费观看 | 成年人免费在线视频 | 久久久www成人免费精品 | 一区二区三区精品视频 | 精品久久一区 | 亚洲国产aⅴ精品一区二区 免费观看av | www.色综合 | 涩涩视频在线播放 | 日韩精品一区二区三区高清免费 | 亚洲国产中文字幕 | 欧美一二区 | 欧美精品a∨在线观看不卡 国产精品久久国产精品 | 国产激情精品视频 | 天天干天天草 | 欧美韩一区二区三区 | 亚洲精品在线看 | 在线一区 | 小h片免费观看久久久久 | 久草精品视频 | 精品在线一区二区 | 国产在线二区 | 欧美一区二区三区在线播放 | av在线一区二区三区 | 久久精品国产99国产精品亚洲 | 毛片区| 国产成人高清视频 | 天天干夜夜操 | 欧美一区二区三区在线看 | av香蕉| 国产精品乱码一区二区三区 | 亚洲欧洲精品成人久久奇米网 | 亚洲精品在线观看视频 | 国产欧美在线观看 | 特级做a爱片免费69 精品国产鲁一鲁一区二区张丽 | 国产九九九九 | 一区视频在线 | 毛片a区| 美女国产精品 | 二区成人|