虛擬函式的記憶體佈局(上)

xuxionglong發表於2020-04-04

C++中 的虛擬函式的作用主要是實現了多型的機制。關於多型,簡而言之就是用父型別別的指標指向其子類的例項,然後通過父類的指標呼叫實際子類的成員函式。這種技術 可以讓父類的指標有“多種形態”,這是一種泛型技術。所謂泛型技術,說白了就是試圖使用不變的程式碼來實現可變的演算法。比如:模板技術,RTTI技術,虛擬函式技術,要麼是試圖做到在編譯時決議,要麼試圖做到執行時決議。

 

 

關於虛擬函式的使用方法,我在這裡不做過多的闡述。大家可以看看相關的C++的書籍。在這篇文章中,我只想從虛擬函式的實現機制上面為大家 一個清晰的剖析。

 

當然,相同的文章在網上也出現過一些了,但我總感覺這些文章不是很容易閱讀,大段大段的程式碼,沒有圖片,沒有詳細的說明,沒有比較,沒有舉一反三。不利於學習和閱讀,所以這是我想寫下這篇文章的原因。也希望大家多給我提意見。

 

言歸正傳,讓我們一起進入虛擬函式的世界。

 

 

虛擬函式表

 

C++ 瞭解的人都應該知道虛擬函式(Virtual Function)是通過一張虛擬函式表(Virtual Table)來實現的。簡稱為V-Table。 在這個表中,主是要一個類的虛擬函式的地址表,這張表解決了繼承、覆蓋的問題,保證其容真實反應實際的函式。這樣,在有虛擬函式的類的例項中這個表被分配在了 這個例項的記憶體中,所以,當我們用父類的指標來操作一個子類的時候,這張虛擬函式表就顯得由為重要了,它就像一個地圖一樣,指明瞭實際所應該呼叫的函式。

 

這裡我們著重看一下這張虛擬函式表。在C++的標準規格說明書中說到,編譯器必需要保證虛擬函式表的指標存在於物件例項中最前面的位置(這是為了保證正確取到虛擬函式的偏移量)。 這意味著我們通過物件例項的地址得到這張虛擬函式表,然後就可以遍歷其中函式指標,並呼叫相應的函式。

 

聽我扯了那麼多,我可以感覺出來你現在可能比以前更加暈頭轉向了。 沒關係,下面就是實際的例子,相信聰明的你一看就明白了。

 

假設我們有這樣的一個類:

 

class Base {

     public:

            virtual void f() { cout << "Base::f" << endl; }

            virtual void g() { cout << "Base::g" << endl; }

            virtual void h() { cout << "Base::h" << endl; }

 

};

 

按照上面的說法,我們可以通過Base的例項來得到虛擬函式表。 下面是實際例程:

 

          typedef void(*Fun)(void);

            Base b;

 

            Fun pFun = NULL;

 

            cout << "虛擬函式表地址:" << (int*)(&b) << endl;

            cout << "虛擬函式表第一個函式地址:" << (int*)*(int*)(&b) << endl;

 

            // Invoke the first virtual function

            pFun = (Fun)*((int*)*(int*)(&b));

            pFun();

 

實際執行經果如下:(Windows XP+VS2003, Linux 2.6.22 + GCC 4.1.3)

 

虛擬函式表地址:0012FED4

虛擬函式表第一個函式地址:0044F148

Base::f

 

 

通過這個示例,我們可以看到,我們可以通過強行把&b轉成int *,取得虛擬函式表的地址,然後,再次取址就可以得到第一個虛擬函式的地址了,也就是Base::f(),這在上面的程式中得到了驗證(把int* 強制轉成了函式指標)。通過這個示例,我們就可以知道如果要呼叫Base::g()Base::h(),其程式碼如下:

 

            (Fun)*((int*)*(int*)(&b)+0); // Base::f()

            (Fun)*((int*)*(int*)(&b)+1); // Base::g()

            (Fun)*((int*)*(int*)(&b)+2); // Base::h()

 

這個時候你應該懂了吧。什麼?還是有點暈。也是,這樣的程式碼看著太亂了。沒問題,讓我畫個圖解釋一下。如下所示:

 

 

 

注意:在上面這個圖中,我在虛擬函式表的最後多加了一個結點,這是虛擬函式表的結束結點,就像字串的結束符“/0”一樣,其標誌了虛擬函式表的結束。這個結束標誌的值在不同的編譯器下是不同的。在WinXP+VS2003下,這個值是NULL。而在Ubuntu 7.10 + Linux 2.6.22 + GCC 4.1.3下,這個值是如果1,表示還有下一個虛擬函式表,如果值是0,表示是最後一個虛擬函式表。

 

 

下面,我將分別說明“無覆蓋”和“有覆蓋”時的虛擬函式表的樣子。沒有覆蓋父類的虛擬函式是毫無意義的。我之所以要講述沒有覆蓋的情況,主要目的是為了給一個對比。在比較之下,我們可以更加清楚地知道其內部的具體實現。

 

一般繼承(無虛擬函式覆蓋)

 

下面,再讓我們來看看繼承時的虛擬函式表是什麼樣的。假設有如下所示的一個繼承關係:

 

 

 

請注意,在這個繼承關係中,子類沒有過載任何父類的函式。那麼,在派生類的例項中,其虛擬函式表如下所示:

 

 

 

對於例項:Derive d; 的虛擬函式表如下:

 

 

 

我們可以看到下面幾點:

1)虛擬函式按照其宣告順序放於表中。

2)父類的虛擬函式在子類的虛擬函式前面。

 

我相信聰明的你一定可以參考前面的那個程式,來編寫一段程式來驗證。

相關文章