前言

我們說的虛表其實有很多種叫法:

virtual method table(VMT)

virtual function table(vftable)

virtual call table

dispatch table

vtable

這些都是虛表的意思。虛表是一種利用程序語言實現的dynamic dispatch機制,或者說runtime method binding機制,也就是我們說的多態。

虛函數

用virtual關鍵字修飾的函數就叫虛函數

因為vTable(虛表)是C++利用runtime來實現多態的工具,所以我們需要藉助virtual關鍵字將函數代碼地址存入vTable來躲開靜態編譯期。這裡我們先不深入探究,後面我會細說。

首先我們先來看一個沒有虛函數,即沒有用到vTable的例子:

#include <iostream>

#include <ctime>using std::cout;using std::endl;struct Animal { void makeSound() { cout << "動物叫了" << endl; } };

struct Cow : public Animal { void makeSound() { cout << "牛叫了" << endl; } };

struct Pig : public Animal { void makeSound() { cout << "豬叫了" << endl; } };struct Donkey : public Animal { void makeSound() { cout << "驢叫了" << endl; } };int main(int argc, const char * argv[]){ srand((unsigned)time(0)); int count = 4; while (count --) { Animal *animal = nullptr; switch (rand() % 3) {

case 0:

animal = new Cow; break; case 1: animal = new Pig; break; case 2: animal = new Donkey; break; }

animal->makeSound();

delete animal; } return 0;}

程序中有一個基類Animal,它有一個makeSound()函數。有三個繼承自Animal的子類,分別是牛、豬、驢,並且實現了自己的makeSound()方法。很簡單的代碼,是吧。

我們運行程序,你覺得輸出結果會是什麼呢?不錯,這裡會連續執行4次Animal的makeSound()方法!為什麼?因為我們的基類Animal的makeSound()方法沒有使用Virtual修飾,所以在靜態編譯時就makeSound()的實現就定死了。調用makeSound()方法時,編譯器發現這是Animal指針,就會直接jump到makeSound()的代碼段地址進行調用。

ok,那麼我們把Animal的makeSound()改為虛函數,如下:

struct Animal {

virtual void makeSound() {

cout << "動物叫了" << endl;

}};

運行會是怎樣?會如你所料,多態已經實現。

接下來就是大家最關心的部分,這是怎麼回事?編譯器到底做了什麼?

虛表

為了說明方便,我們需要修改一下基類Animal的代碼,不改變其他子類,修改如下:

struct Animal {

virtual void makeSound() { cout << "動物叫了" << endl; } virtual void walk() {} void sleep() {}};

struct Cow : public Animal { void makeSound() { cout << "牛叫了" << endl; } };

struct Pig : public Animal { void makeSound() { cout << "豬叫了" << endl; } };struct Donkey : public Animal { void makeSound() { cout << "驢叫了" << endl; } };

首先我們需要知道幾個關鍵點:

(1)函數只要有virtual,我們就需要把它添加進vTable。

(2)每個類(而不是類實例)都有自己的虛表,因此vTable就變成了vTables。

(3)虛表存放的位置一般存放在模塊的常量段中,從始至終都只有一份。詳情可在此參考

我們怎麼理解?從本例來看,我們的Animal、Cow、Pig、Donkey類都有自己的虛表,並且虛表裡都有兩個地址指針指向makeSound()和walk()的函數地址。一個指針4個位元組,因此每個vTable的大小都是8個位元組。

他們的虛表中記錄著不同函數的地址值。可以看到Cow、Pig、Donkey重寫了makeSound()函數但是沒有重寫walk()函數。因此在調用makeSound()時,就會直接jump到自己實現的code Address。而調用walk()時,則會jump到Animal父類walk的Code Address。

虛指針

現在我們已經知道虛表的數據結構了,那麼我們在堆里實例化類對象時是怎麼樣調用到相應的函數的呢?這就要藉助到虛指針了(vPointer)。

虛指針是類實例對象指向虛表的指針,存在於對象頭部,大小為4個位元組。

我們修改main函數里的代碼,如下:

int main(int argc, const char * argv[])

{ int count = 2; while (count --) { Animal *animal = new Donkey; animal->makeSound(); delete animal; } return 0;

}

我們在堆中生成了兩個Donkey實例,運行結果如下:

推薦閱讀:

相关文章