咱們說的虛表其實有不少種叫法:ios
這些都是虛表的意思。虛表是一種利用程序語言實現的dynamic dispatch機制,或者說runtime method binding機制,也就是咱們說的多態。數據結構
注:筆者在本文使用C++語言,而且統一用vTable來表示虛表。函數
用virtual關鍵字修飾的函數就叫虛函數。工具
由於vTable(虛表)是C++利用runtime來實現多態的工具,因此咱們須要藉助virtual關鍵字將函數代碼地址存入vTable來躲開靜態編譯期。這裏咱們先不深刻探究,後面我會細說。spa
首先咱們先來看一個沒有虛函數,即沒有用到vTable的例子:.net
#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; }指針
1code 2對象 3blog 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
#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; } };
1 2 3 4 5 6 |
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 2 3 4 5 6 7 8 9 |
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; } }; |
首先咱們須要知道幾個關鍵點:
咱們怎麼理解?從本例來看,咱們的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個字節,好比咱們的Donkey類的實例化對象數據結構就以下:
咱們修改main函數裏的代碼,以下:
int main(int argc, const char * argv[]) { int count = 2; while (count --) { Animal *animal = new Donkey; animal->makeSound(); delete animal; } return 0; }
1 2 3 4 5 6 7 8 9 10 |
int main(int argc, const char * argv[]) { int count = 2; while (count --) { Animal *animal = new Donkey; animal->makeSound(); delete animal; } return 0; } |
咱們在堆中生成了兩個Donkey實例,運行結果以下:
驢叫了 驢叫了 Program ended with exit code: 0
1 2 3 |
驢叫了 驢叫了 Program ended with exit code: 0 |
沒問題。而後咱們再來看看堆裏的結構,就變成了這樣:
還有什麼是這張圖不能說明的呢?