當類中定義有虛函數時,編譯器會將該類中全部虛函數的首地址保存在一張地址表中,這張表被稱爲虛函數地址表。編譯器還會在類中添加一個虛表指針。html
舉例:數組
CVirtual類的構造函數中沒有進行任何操做,可是咱們來看構造函數內部,仍是有一個賦初值的操做:函數
這個地址指向的是一個數組:this
這些數組中的內容就是虛函數的指針:spa
值得注意的是,若是沒有虛指針的存在,那麼CVirtual大小就是4字節。有了這個指針存在就是8字節。.net
本例子中,使用了一個空的構造函數,可是編譯器本身擅自插入了代碼,實現了對虛表的初始化。若是咱們沒有提供任何構造函數的話,那麼編譯器就會提供一個默認的構造函數對虛表進行初始化。指針
當函數被調用時,會間接訪問虛表,獲得對應的虛函數的地址,並調用執行。這種經過虛表間接尋址訪問的狀況只有在使用對象的指針或引用來調用虛函數的時候纔會出現。當直接使用對象調用自身的虛函數時,沒有必要查表訪問。由於已經明確調用的是自身的成員函數了,根本沒有構成多態性。htm
舉例:對象
直接經過對象調用虛函數的時候,就是直接用對象的地址做爲隱含參數傳遞給這個虛函數:blog
這個虛函數此時和普通的成員函數沒有區別。之因此要隱含傳遞對象的地址,是爲了可以準確適用對象中所包含的數據成員。
可是若是構成了多態,調用方式就不一樣了:
由於你實際上不知道pcv指針指向的具體類型是什麼,因此要到虛表中找到所指向的真正的對象的那個虛函數。
虛表指針的初始化,是判斷一個函數是構造函數的充分條件。
析構函數對虛表如何操做?在考慮這個問題以前,咱們先要知道,爲何析構函數要使用虛函數:
若是父類和子類的虛函數分別以下所示:
咱們在執行delete指針以後,會是以下流程:
即只調用了父類的虛函數。而若是把析構函數設置爲虛的:
則是以下調用流程:
delete刪除指針的時候調用的是子類的虛函數,而子類的虛函數內部又調用了父類的虛函數。而調用父類的虛函數以前,ecx指針中仍保留的是子類對象的首地址:
子類的析構函數調用自身的虛成員函數:
隨後調用父類的析構函數:
父類的析構函數中沒有間接尋址,直接調用了Show1和Show2:
可是不管是子類仍是父類的虛析構函數中都會有這麼一步操做:把當前類的虛表的首地址賦值到虛表指針當中去。這是爲了防止在析構函數中調用虛函數時取到非自身的虛表。爲何要這麼作?舉例說明:
先調用A的構造函數:
A類填充虛表:
調用虛函數:
調用完A的構造函數,繼續往下執行B的構造函數中的其他部分,爲了可以正常調用B的func2,這裏必需要還原虛表:
析構函數中同理。
1)特徵:
一、類中隱式定義了一個數據成員;
二、該數據成員在首地址處;
三、構造函數會將此數據成員初始化爲某個數組的首地址;
四、這個地址屬於數據區,是相對固定的地址;
五、數組內每一個元素都是函數的指針;
六、數組中的這些函數被調用時,第一個參數必然是this指針;
七、這些函數內部有可能對this指針進行相對間接的訪問。
2)驗證父類和子類的虛表指針:
舉例:
初始化父類以後:
父類的兩個虛函數地址爲:
調用完父類構造函數以後會從新賦值一個虛表:
咱們發現,A的虛表中和B的虛表中的第一個函數地址是相同的,不一樣的是第二個函數的地址。在構造B的時候先構造A,而在構造A的時候要賦值一個虛表指針,是爲了防止在A的構造函數中使用使用了虛函數,而無心間調用了B的虛函數。而實際上,構造完B以後,B中就不存在剛剛A的那個虛表指針了。
藉助OD找到A虛表的地址和B虛表的地址:
因而,先根據交叉引用找到了A的構造函數:
再借助交叉引用找到B的構造函數:
固然若是B中有多個構造函數,和一個析構函數時是什麼狀況呢?會有兩個構造函數引用它:
舉例:
全局對象的構造函數調用以後會調用一個函數來登記析構函數的地址:
此時push進去的參數是一個函數的地址41A4D0:
進入call 4110F5,這裏邊的call 4158A8就是等同於atexit的做用:
裏邊會把這個函數的地址傳遞給onexit:
參考:
http://bbs.csdn.net/topics/360161935
https://www.2cto.com/kf/201408/326530.html
參考這兩個連接瞭解到,傳遞給onexit的函數,總會在main函數執行完畢以後執行。因此能夠推斷出41A4D0是個析構函數。
看下41A4D0中的內容:
進入被標記的那個call,發現這就是一個析構函數,而且有一個恢復虛表指針的操做:
交叉引用看一下哪些地方用到了這個虛表:
發現只有兩個交叉引用,左邊的那個是析構函數,右邊的那個是構造函數。雖然這個全局類包括了三種形式的構造函數,可是咱們的程序中只用了一種形式,因此只有其中一種形式的構造函數對虛表進行了操做: