1. 程式人生 > >C++ 中的虛擬函式表及虛擬函式執行原理

C++ 中的虛擬函式表及虛擬函式執行原理

為了實現虛擬函式,C++ 使用了虛擬函式表來達到延遲繫結的目的。虛擬函式表在動態/延遲繫結行為中用於查詢呼叫的函式。 儘管要描述清楚虛擬函式表的機制會多費點口舌,但其實其本身還是比較簡單的。 首先,每個包含虛擬函式的類(或者繼承自的類包含了虛擬函式)都有一個自己的虛擬函式表。這個表是一個在編譯時確定的靜態陣列。虛擬函式表包含了指向每個虛擬函式的函式指標以供類物件呼叫。 其次,編譯器還在基類中定義了一個隱藏指標,我們稱為 `*__vptr`,`*__vptr` 是在類例項建立時自動設定的,以指向類的虛擬函式表。`*__vptr` 是一個真正的指標,這和 `*this` 指標不同,`*this` 指標實際是一個函式引數,使編譯器來達到自引用的目的。 結果就是,每個類物件都會多分配一個指標的大小,並且 `*__vptr` 是被派生類繼承的。 如果你不清楚這些元件是怎麼配合運作的,看下面的例子: ```cpp class Base { public: virtual void function1() {}; virtual void function2() {}; }; class D1: public Base { public: virtual void function1() {}; }; class D2: public Base { public: virtual void function2() {}; }; ``` 因為這裡有 3 個類,編譯器會建立 3 個虛擬函式表。 然後編譯器會在使用了虛擬函式的最上層基類中定義一個隱藏指標。儘管這個過程編譯器會自動處理,但我們還是通過下面的例子來說明指標新增的位置: ```cpp class Base { public: FunctionPointer *__vptr; virtual void function1() {}; virtual void function2() {}; }; class D1: public Base { public: virtual void function1() {}; }; class D2: public Base { public: virtual void function2() {}; }; ``` `*__vptr` 在類物件建立的時候會設定成指向類的虛擬函式表。例如,型別 `Base` 被例項化的時候,`*__vptr` 就指向 `Base` 的虛擬函式表。型別 `D1` 或者 `D2` 被例項化的時候,`*__vptr` 就指向 `D1` 或者 `D2` 的虛擬函式表。 現在我們來看下虛擬函式表是怎麼建立的。因為示例中每個類僅有 2 個虛擬函式,所以每個虛擬函式表會存放兩個函式指標(分別指向 `function1()` 和 `function2()`)。 `Base` `物件的虛擬函式表最簡單。Base` 物件只能訪問 `Base` 型別的成員,不能訪問 `D1` 或者 `D2` 的函式。所以 `Base` 的虛擬函式表中的兩個指標分別指向 `Base::function1()` 和 `Base::function2()`。 `D1` 的虛擬函式表稍複雜點,`D1` 物件能夠訪問 `D1` 以及 `Base` 的成員。`D1` 重寫了 `function1()`,但沒有重寫 `function2()`,所以 `D1` 的虛擬函式表中的兩個指標分別指向 `D1::function1()` 和 `Base::function2()`。 `D2` 的虛擬函式表同理 `D1`,包含了分別指向 `Base::function1()` 和 `D2::function2()` 的指標。 ![Virtual Table](https://img2020.cnblogs.com/blog/711185/202103/711185-20210305142222787-2030166653.gif)
考慮如果建立 `D1` 物件時會發生什麼: ```cpp int main() { D1 d1; } ``` 因為 `d1` 是 `D1` 型別物件,`d1` 有它自己的 `*__vptr` 指向 `D1` 型別的虛擬函式表。 現在建立一個 `Base` 型別指標 `*dPtr` 指向 `d1`: ```cpp int main() { D1 d1; Base *dPtr = &d1; return 0; } ``` 重點: > 因為 `dPtr` 是 `Base` 型別指標,它只指向 `d1` 物件的 `Base` 型別部分(即,指向 `d1` 物件中的 `Base` 子物件),而 `*__vptr` 也在 `Base` 型別部分。所以 `dPtr` 可以訪問 `Base` 型別部分中的 `*__vptr`。同時,這裡注意,`dPtr->__vptr` 指向的是 `D1` 的虛擬函式表,這是在 `d1` 初始化時就確定的。所以結果,儘管 `dPtr` 是 `Base` 型別指標,但它能夠訪問 `D1` 的虛擬函式表。 因此,當有呼叫 `dPtr->function1()` 時,發生了什麼? ```cpp int main() { D1 d1; Base *dPtr = &d1; dPtr->function1(); return 0; } ``` 首先,程式識別到 `function1()` 是一個虛擬函式。
其次,程式使用 `dPtr->__vptr` 獲取到了 `D1` 的虛擬函式表。
然後,它在 `D1` 的虛擬函式表中尋找可以呼叫的 `function1()` 版本,這裡是 `D1::function1()`。
因此,`dPtr->function1()` 實際呼叫了 `D1::function1()`。 通過虛擬函式表,編譯器和程式能夠確定呼叫什麼版本的虛擬函式,儘管使用的是指向/引用基類的指標或者引用。 呼叫虛擬函式會比呼叫非虛擬函式更慢,有以下幾個原因: - 必須使用 `*__vptr` 獲取正確的虛擬函式。 - 必須建立虛擬函式表的索引來獲取想要呼叫的函式。 - 呼叫找到的函式。 結果就是必須進行三次操作才能完成對函式的呼叫。但是對於現代計算機系統,這些額外操作增加的時間幾乎可以忽略不計。 另外,每個使用虛擬函式表的類都有 `*__vptr` 指標,從而每個類物件都會多一個指標的空間。虛擬函式很強大,但是它確實產生了效能