標籤:析構 編譯器 位元組 對齊 隱藏 自身 oid 添加 aaa
1.無繼承的普通類:
在有虛函數的情況下類會為其增加一個隱藏的成員,虛函數表指標,指向一個虛函數表,虛函數表裡面就是類的各個虛函數的地址了。那麼,虛函數表指標是以什麼模型加入到類裡面的,虛函數表裡面又是怎麼安排的呢。簡單來看下就可以知道了。
#include"stdafx.h" #pragma pack(8) class A{ public: int a; double a2; A() :a(0xaaaaaaaa), a2(0){} virtual void funA2(){} virtual ~A(){} virtual void funA(){} }; int _tmain(int argc, _TCHAR* argv[]) { A a; return 0; }
定義一個A的變數,然後看其記憶體布局:
最開始的 4個位元組就是虛函數表指標了,類A中有double類型的成員變數 a2,所以類 A的有效位元組對齊數是 8,因此可以看到在虛函數表指標後又填充了 4個位元組。放完虛函數表指標然後才到類 A 的成員變數。所以在普通類裡面,如果有虛函數的話就會在最開始的地方添加一個隱藏的成員變數,虛函數表指標,然後才到正常的成員變數。然後我們再去看下虛函數表裡面是什麼樣子的:
虛函數表也是以4位元組為一項,每一項儲存一個虛函數的地址。儲存的虛函數的地址按照函式宣告的順序排放,第一項存放第一個聲明的虛函數,第二項存放第二個,依此類推。我們看下這個表裡面的每個項都是什麼。
依次選擇:調試 --> 視窗 --> 反組譯碼,開啟彙編視窗,可以看到來源程式的彙編代碼。
我們先來看第一個虛函數:
virtual void funA2(){}
由上可知,該函數的地址是:0x00d41028(注意是小端序),在彙編視窗中找到該地址:
看到0x00d41028 處放置了一條 jmp 指令,virtual void funA2() 的真正地址是 0x00d41550
我們可以在彙編視窗中找到 0x00d41550地址,結果如下:
可以看到這虛函數表中的每一項地址實際上並不是虛函數的直接地址,而是一個跳轉到相應虛函數的地址。
所以在有虛函數的情況下類的安排也是很簡單的,和沒有虛函數的情況相比就是在最前面加一個虛函數表指標而已。其他的東西就和沒有虛函數的類的情況的時候一樣了。然後好像也沒有什麼然後了,複雜的是在後面~
2.單繼承的情況:
單繼承大概又可以分為兩種情況,一種是基類沒有虛函數的情況,一種是基類已經有虛函數表指標的情況。我們分別來看下。
2.1 基類無虛函數的單繼承
#include "stdafx.h" #pragma pack(8) class F2{ public: int f2; double f22; F2() :f2(0xf2f2f2f2), f22(0){} }; class B : F2{ public: int b; B() :b(0xbbbbbbbb){} virtual void funB(){} }; int _tmain(int argc, _TCHAR* argv[]) { B b; return 0; }
B的布局抓資料如下:
可以看到虛函數表指標還是放在最開始的地方,也遵循它自己的地址對齊規則,主動填充了4個位元組在後面。然後就是F2作為一個整體結構存放在其後,最後才是成員變數b,整個結構也要自身對齊,所以填充了4個位元組在最後。虛函數表裡面的就是B的虛函數funB的地址了。因為只有一個虛函數,所以虛函數表裡面也就只有一項。
同樣,我們開啟反組譯碼視窗,找到 0x012e1221 地址處:
可以看到 0x012e1221處放置了一條 jmp 指令,virtual void funB(){} 的真正地址是 0x012e14e0
我們可以在彙編視窗中找到 0x012e14e0地址,結果如下:
果然是 virtual void funB(){} 的起始位置~
所以在基類沒有虛函數的情況下,會產生一個虛函數表指標,而且也還是先存放類的虛函數表指標,然後才到基類等。其實在類有虛函數的情況下(暫不考慮虛繼承),虛函數表指標都是會存放在最開始的。我們再來看下如果繼承的基類已經有了虛函數表指標的情況會是什麼樣子。
2.2 基類有虛函數的單繼承
#include "stdafx.h" #pragma pack(8) class A { public: int a; double a2; A() :a(0xaaaaaaaa), a2(0){} virtual void funA2(){} virtual ~A(){} virtual void funA(){} }; class B : A{ public: int b; B() :b(0xbbbbbbbb){} virtual void funB(){} virtual void funA2(){} }; int _tmain(int argc, _TCHAR* argv[]) { B b; return 0; }
A的布局我們已經知道了,現在B繼承A,而且還有覆蓋了A的虛函數,來看下布局。
很明顯,在基類已經有虛函數表指標的情況下衍生類別不會再主動產生一個虛函數表指標,基類的虛函數表指標是可以和衍生類別共用的,因為基類的虛函數肯定也是屬於衍生類別的,如果衍生類別有虛函數覆蓋掉基類的虛函數的話就會把虛函數表裡面的相應的項改成正確的地址,而且虛函數表指標剛好也是放在類的最開始的位置。所以在這種情況下就是先放基類然後再排放成員變數。我們來看下現在衍生類別和基類共用的虛函數表是什麼樣子的。
虛指標表中共有 4 項,像前面的分析方法一樣,我們結合反組譯碼視窗,可以得出如下結論(注意是小端序):
虛函數表有4個項:
1、 第一個項的虛函數已經被B裡面的那個funA2所取代了,因為B裡面的funA2已經覆蓋了基類A裡面的funA2,所以在虛函數表裡面也要相應的改變,這也正是虛函數得以正確調用的前提。
2、 第二個項,也被替換成了B的虛解構函式,我們在代碼裡面沒明寫出B的虛解構函式,編譯器會自動產生一個,而且B的虛解構函式也是會覆蓋掉基類A的虛解構函式的。
3、 第三項還是A裡面的函數funA,因為在衍生類別裡面沒有被覆蓋,所以還應該是基類裡面的函數。
4、 第四項是基類A沒有的函數funB,所以在這個共用的虛函數表裡面基類A只是用到了前3項而已,後面的項就是沒有覆蓋掉基類的其他虛函數了,而且是按照聲明順序依次排放的。
所以我們暫時可以得出的結論是,有虛函數的類在單繼承的情況下,如果基類沒有虛函數表指標的話會產生一個隱藏的成員變數,虛函數表指標,放在類的最前面,然後才是基類,最後是衍生類別的各個成員;如果基類已經有了虛函數表指標的話就不需要再產生一個虛函數表指標,衍生類別可以和基類共用一個虛函數表,此時衍生類別的布局是先放基類然後再放衍生類別的各個成員變數。如果衍生類別有函數覆蓋了基類裡面的虛函數的話,虛函數表裡面的相應項就會改成這個函數的真正地址,其他沒有覆蓋的虛函數按照聲明的順序依次排放在虛函數表的後面各項中。
3.多繼承的情況
鑒於有虛函數的類的第一項都要是虛函數表指標,所以在多繼承的情況下會跟普通情況有所不同。但是有虛函數的類多繼承情況下的物件模型也還是比較簡單和明確的。
大概也有兩種情況,一種是所有的基類都沒有虛函數的情況,一種是基類中有些有虛函數有些又沒有虛函數的混雜情況。
對於第一種情況,記憶體布局大概是這樣,比如類A的基類都是沒有虛函數的話
class A:F0,F1,F2{int a; (其他成員變數)…… virtual voidfun1(){} ……};
那麼A肯定也還是要產生一個虛函數表指標的,放在最開始的位置,這種情況下的等價模型大概是這樣 :
class A{void * vf_ptr;F0{};F1{};F2{};int a; (其他成員變數)……};
注意各個的位元組對齊就可以了,特別是虛函數表指標。
對於第二種情況,基類是混雜的情況的時候,比如類A:
class A : F0, F1, V0, V1, F2, V2 { int a; (其他成員變數)…… virtual void fun1(){} ……};
V0、V1、V2是有虛函數的基類,F是沒虛函數的基類,而且繼承的聲明順序隨意。像這種情況的話類A的物件模型大概是這樣的:先排放基類中有虛函數的基類,按照聲明順序,然後再排放基類中沒有虛函數的基類,也是按照聲明順序。比如A此時的物件模型就大概是這樣:
class A{V0{};V1{};V2{};F0{};F1{};F2{};int a; (其他成員變數)……};
因為基類已經有了虛函數表指標了,所以衍生類別A也是可以和第一個有虛函數表指標的基類共用一個虛函數表的,這個和單繼承的時候的道理是一樣的,自然衍生類別就不會在產生一個虛函數表指標了。我們來實際來下這兩種情況的執行個體。
3.1 基類沒有虛函數
#include"stdafx.h" #pragma pack(8) class F0{ public:char f0; F0() :f0(0xf0){} }; class F1{ public:int f1; F1() :f1(0xf1f1f1f1){} }; class C : F1, F0{ public: int c; virtual void funC(){} virtual void funB(){} virtual void funA2(){} C() :c(0x33333333){} }; int _tmain(int argc, _TCHAR* argv[]) { C c; return 0; }
在衍生類別有虛函數而基類都沒有虛函數的情況下,衍生類別仍然會產生一個虛函數表指標放在最開始,然後才到各個基類,最後就是成員變數了。結合反組譯碼視窗,來看下虛函數表裡面是些什麼。
虛函數表指標共有 3 項,像前面的分析方法一樣,我們結合反組譯碼視窗,可以得出如下結論(注意是小端序):
可以看到由於衍生類別的虛函數沒有覆蓋任何基類裡面的虛函數所以虛函數表裡面的各項就是各個虛函數按照聲明的順序的地址了。然後再來看下基類有虛函數而且衍生類別還有覆蓋掉基類的虛函數的情況。
3.2 基類中有虛函數
#include"stdafx.h" #pragma pack(8) class F0{ public:char f0; F0() :f0(0xf0){} }; class F1{ public:int f1; F1() :f1(0xf1f1f1f1){} }; class A { public: int a; double a2; A() :a(0xaaaaaaaa), a2(0){} virtual void funA2(){} virtual ~A(){} virtual void funA(){} }; class B : A{ public: int b; B() :b(0xbbbbbbbb){} virtual void funB(){} virtual void funA2(){} }; class C : F1, A,F0, B{ public: int c; virtual void funC(){} virtual void funB(){} virtual void funA2(){} C() :c(0x33333333){} }; int _tmain(int argc, _TCHAR* argv[]) { C c; return 0; }
類C的模型大概是這樣:
class C{ public: A a; B b; F1 f1; F0 f0; int c; };
很明顯,雖然F1聲明在基類的最前面但是存放順序還是先存放有虛函數的基類A然後到也是有虛函數的基類B,再才是各個沒有虛函數的基類F1、F0。最後才是衍生類別C的成員變數。C的虛函數funB 覆蓋了基類B裡面的虛函數,而另一個虛函數funA2既覆蓋了基類A裡面的虛函數也覆蓋了基類B繼承自基類A裡面的虛函數funA2,理論上基類A和基類B裡面被覆蓋掉的虛函數其在各自虛函數表裡面的對應項都要被改變成正確的函數地址,也就是C裡面的虛函數的真真實位址。然後我們看下A和B的虛函數表是什麼樣子的。
A和C共用的虛函數表:
虛函數表指標共有 4項,像前面的分析方法一樣,我們結合反組譯碼視窗,可以得出如下結論(注意是小端序):
B的虛函數表:
虛函數表指標共有 4項,像前面的分析方法一樣,我們結合反組譯碼視窗,可以得出如下結論(注意是小端序):
可以看到衍生類別和基類A共用的虛函數表裡面的各個項已經修改成了函數的真正的地址,在最後還加了一個沒有覆蓋掉任何基類虛函數的虛函數地址項。而基類B裡面的項就有點意外了,它並不是直接修改成跳轉到正確的地址上去,而是使用了一個調整塊的東西,把EAX寄存器減去相應的值,然後再跳轉到正確的函數裡面去,這個暫時不在這裡贅述,反正最後還是跳轉到了C裡面的那個函數裡面去就是了。其他的項有覆蓋的也還是一樣都要修改成正確的函數地址。
******************************************************************************
virtual void funA2(){} virtual ~A(){} virtual void funA(){} virtual void funB(){} virtual void funA2(){} virtual void funC(){} virtual void funB(){} virtual void funA2(){} c::funA2C::~CA:funAc:func至於上面為什麼沒有C:FUNB,因為B類中有的A類中沒有,所有c類不需要替換,
相反對於funA2,A,B,C三個類中都有,那麼我們優先選著A類的c:funa2c:~cA:funAC:funB
C++ 虛函數的記憶體配置