深入C++虛表

福建電腦
2010年第2期(下轉第76頁)
                                                                                                              深入C++虛表
                                                                                                                              董士珍
                                                                                                                                                                                             (中煤國際工程集團武漢設計研究院電氣所湖北武漢430064)
【摘要】:多態是C++語言中最重要的特性之一,而虛表以及虛函數是實現多態的重要手段。許多C++語言的教材對於虛函數的使用以及調用機制有着詳細的闡述,但是對於虛表的一些細節內容闡述卻並不是很深,對於虛表我們可能會有很多疑問。本文就試圖通過使用彙編語言對於虛表實現的細節進行分析,從而加深對多態機制的理解。


【關鍵詞】:C++,虛函數,虛表,反彙編一、引言

數據抽象,繼承以及多態是構成C++面向對象編程思想的三個核心特性。在C++中,多態是通過函數調用的遲綁定來實現的,而遲綁定又是通過虛函數的運行時調用來實現的。具體來說,對於有虛函數聲明的類,它的每個對象都擁有一個指向虛表的指針。虛表中存放的是類的所有虛函數地址。在調用虛函數的時候,首先找到虛表指針,通過虛表指針找到虛表,然後在虛表中查詢到需要調用的虛函數地址,最終調用這個虛函數。由於這一切都是編譯器在幕後完成的,我們對虛表的瞭解比較少。圍繞着它我們可能有許多疑惑:

·

虛表是怎麼實現的?虛表存放在哪裏?·虛表中的數據是在什麼時候確定的?

·對象中的虛表指針又在什麼時候賦值的?
我們很難通過C++語言本身來找到答案。C++標準給編譯器實現者定義了語法規範,但是被並沒有定義如何實現這些語法規範,不同的編譯器實現者可能有不同的實現方法,可以肯定的是他們的編譯器必須符合這些語法規範。彙編語言作爲最接近機器語言的計算機語言,可以爲我們揭示一些隱藏在編譯器內部的細節。接下來本來就試圖通過對C++源碼進行反彙編的方式來解答這些疑惑。二、分析
這裏我選用WinXP和VS2008作爲我們這次分析的平臺。我們建立一個最簡單的Win32控制檯程序,並定義兩個簡單的類:
接下來我們可以直接編譯這些C++源碼就可以得到相應的彙編代碼。通過分析這些彙編代碼我們就找到許多有用的信息。我們可以找到這樣的彙編代碼:

以上的彙編代碼定義了兩個數據段,而這兩個數據段中的內容恰好就是類的虛表。至此虛表的"廬山真面目"完全展示在我們的面前。根據這些信息,我們可以推理出很多有用的

結論:

·擁有虛函數的類會有一個虛表,而且這個虛表存放在類定義模塊的數據段中。模塊的數據段通常存放定義在該模塊的全局數據和靜態數據,這樣我們可以把虛表看作是模塊的全局數據或者靜態數據


·類的虛表會被這個類的所有對象所共享。類的對象可以有很多,但是他們的虛表指針都指向同一個虛表,從這個意義上說,我們可以把虛表簡單理解爲類的靜態數據成員。值得注意的是,雖然虛表是共享的,但是虛表指針並不是,類的每一個對象有一個屬於它自己的虛表指針。


·虛表中存放的是虛函數的地址,正好也驗證了C++教材中的說法。

        另外一個大的疑惑就是對象的虛表指針是在什麼時候被賦值的?我們都知道,類的對象是通過構造函數來完成初始化的,但是我們從來沒有在構造函數中初始化虛表指針,那麼編譯器在幕後又做了哪些事情呢?我們依然還是通過反彙編來找到答案。在這個控制檯程序的main函數中我們構建一個類對象:
依然是查看編譯後的彙編代碼,我們又一次幸運的找到有用的信息:
我們知道類的非靜態成員函數調用時,編譯器會傳入一個"隱藏"的參數。這個參數就是通常我們說的"this"指針,它的值就是對象的地址。在上面的代碼中,寄存器ECX保存的就是這個"this"指針,同時它的值又賦給了寄存器EAX。"??_7CD-szBase@@6B@"就是上面提到的虛表,同時它也代表了虛表的地址。接下來,虛表的地址被賦給了由寄存器EAX指定的內存中。由此可見,虛表的地址被存放在對象的起始位置,即對象的第一個數據成員就是它的虛表指針。同時我們還可以注意到,虛表指針的初始化確實發生在構造函數的調用過程中,但是在執行構造函數體之前,即進入到構造函數的"{"和"}"之前。爲了更好的理解這一問題,我們可以把構造函數的調用過程細分爲兩個階段,即:
1.進入到構造函數體之間。在這個階段如果存在虛函數的話,虛表指針被初始化。如果存在構造函數的初始化列表的話,初始化列表也會被執行。

2.進入到構造函數體內。這一階段是我們通常意義上說的構造函數。


三.總結

本文深入解讀了一些關於虛表的細節內容,而這些內容反過來加深了我們對虛函數的理解。同時,本文也展現瞭如何使用反彙編技術來探究隱藏在編譯器幕後的祕密。通過熟練地使用反彙編技術,我們還可以探究諸如多繼承下的虛函數調用,動態鏈接庫(DLL)的實現機制等等更復雜的問題。由此可見,我們需要適當地掌握一些彙編語言的知識,這對於我們加深對高級計算機語言的學習以及更好的理解整個計算機體系有很大的幫助。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章