其實這是我前一段時間思考過的一個問題,是在看《深入探索C++對象模型》這本書的時候我產生的一個疑問,最近在網上又看到類似的帖子,貼出來看看:
Answer 1:


我們都知道,虛函數是多態機制的基礎,就是在程序在運行期根據調用的對象來判斷具體調用哪個函數,現在我們來說說它的具體實現原理,主要說一下我自己的理解,如果有什么不對的地方請指正
在每個包含有虛函數的類的對象的最前面(是指這個對象對象內存布局的最前面,至於為什么是最前面,說來話長,這里就不說了,主要是考慮到效率問題)都有一個稱之為虛函數指針(vptr)的東西指向虛函數表(vtbl),這個虛函數表(這里僅討論最簡單的單一繼承的情況,若果是多重繼承,可能存在多個虛函數表)里面存放了這個類里面所有虛函數的指針,當我們要調用里面的函數時通過查找這個虛函數表來找到對應的虛函數,這就是虛函數的實現原理。這里我假設大家都了解了,如果不了解可以去查下資料。好了,既然我們知道了虛函數的實現原理,虛函數指針vptr指向虛函數表vtbl,而且vptr又在對象的最前面,那么我們很容易可以得到虛函數表的地址,下面我寫了一段代碼測試了一下:
#include <iostream> #include <stdio.h> typedef void (*fun_pointer)(void); using namespace std; class Test { public: Test() { cout<<"Test()."<<endl; } virtual void print() { cout<<"Test::Virtual void print1()."<<endl; } virtual void print2() { cout<<"Test::virtual void print2()."<<endl; } }; class TestDrived:public Test { public: static int var; TestDrived() { cout<<"TestDrived()."<<endl; } virtual void print() { cout<<"TestDrived::virtual void print1()."<<endl; } virtual void print2() { cout<<"TestDrived::virtual void print2()."<<endl; } void GetVtblAddress() { cout<<"vtbl address:"<<(int*)this<<endl; } void GetFirstVtblFunctionAddress() { cout<<"First vbtl funtion address:"<<(int*)*(int*)this+0 << endl; } void GetSecondVtblFunctionAddress() { cout<<"Second vbtl funtion address:"<<(int*)*(int*)this+1 << endl; } void CallFirstVtblFunction() { fun = (fun_pointer)* ( (int*) *(int*)this+0 ); cout<<"CallFirstVbtlFunction:"<<endl; fun(); } void CallSecondVtblFunction() { fun = (fun_pointer)* ( (int*) *(int*)this+1 ); cout<<"CallSecondVbtlFunction:"<<endl; fun(); } private: fun_pointer fun; }; int TestDrived::var = 3; int main() { cout<<"sizeof(int):"<<sizeof(int)<<"sizeof(int*)"<<sizeof(int*)<<endl; fun_pointer fun = NULL; TestDrived a; a.GetVtblAddress(); cout<<"The var's address is:"<<&TestDrived::var<<endl; a.GetFirstVtblFunctionAddress(); a.GetSecondVtblFunctionAddress(); a.CallFirstVtblFunction(); a.CallSecondVtblFunction(); return 0; }
這里我們通過得到虛函數表的地址調用了里面的虛函數。
這幾天又查了下資料,終於搞清楚虛函數表vtable在Linux/Unix中存放在可執行文件的只讀數據段中(rodata),這與微軟的編譯器將虛函數表存放在常量段存在一些差別。將上面的文件編譯生成最終的可執行文件,然后利用命令:
objdump -s -x -d a.out | c++filt | grep "vtable" 可以得到以下輸出
可執行文件中的詳細信息,包括可執行文件的header, section, symbol等等,用objdump獲得了可執行文件的符號很多都是
我們看不懂的,或者說與我們源代碼中的函數或者變量不太一樣,這是因為C++支持函數重載,C++對所有的符號都做了
修飾,很多資料稱之為“函數簽名”或者“符號修飾”類似的概念,但是我們要將其轉換為我們源代碼中的符號,這就要用到
c++filt命令了,好了,到這里告一段落了,總之關於虛函數表的具體細節就介紹到這里。
幾個值得注意的問題
- 虛函數表是class specific的,也就是針對一個類來說的,這里有點像一個類里面的staic成員變量,即它是屬於一個類所有對象的,不是屬於某一個對象特有的,是一個類所有對象共有的。
- 虛函數表是編譯器來選擇實現的,編譯器的種類不同,可能實現方式不一樣,就像前面我們說的vptr在一個對象的最前面,但是也有其他實現方式,不過目前gcc 和微軟的編譯器都是將vptr放在對象內存布局的最前面。
- 雖然我們知道vptr指向虛函數表,那么虛函數表具體存放在內存哪個位置呢,雖然這里我們已經可以得到虛函數表的地址。實際上虛函數指針是在構造函數執行時初始化的,而虛函數表是存放在可執行文件中的。下面的一篇博客測試了微軟的編譯器將虛函數表存放在了目標文件或者可執行文件的常量段中,http://blog.csdn.net/vicness/article/details/3962767,不過我在gcc下的匯編文件中沒有找到vtbl的具體存放位置,主要是對可執行文件的裝載和運行原理還沒有深刻的理解,相信不久有了這些知識之后會很輕松的找到虛函數表到底存放在目標文件的哪一個段中。
- 經過測試,在gcc編譯器的實現中虛函數表vtable存放在可執行文件的只讀數據段.rodata中。