**c++中虛函式的呼叫及物件的內部布局1 (來自:lizhe1985)
在我那篇《**c++中的this指標》中,我通過分析c++**編譯後生成的彙編**來分
析this指標的實現方法。這次我依然用分析c++**編譯後生成的彙編**來說明c++中
虛函式呼叫的實現方法,順便也說明一下c++中的物件內部布局。下面所有的彙編**都
是用vc2005編譯出來的。雖然,不同的編譯器可能會編譯出不同的結果,物件的內部布
局也不盡相同;但是,只要是符合c++標準的編譯器,編譯結果和物件的內部布局應該是
大同小異。
首先,是乙個有著簡單繼承關係的兩個類:
class cbase
public:
virtual void vfun1() = 0;
virtual void vfun2() = 0;
void fun1();
// 這裡僅僅是為了生成函式的彙編**,因此函式體為空
void cbase::fun1()
class cderived : public cbase
public:
virtual void vfun1();
virtual void vfun2();
void fun2();
private:
int m_ivalue1;
int m_ivalue2;
// 這裡僅僅是為了生成函式的彙編**,因此函式體為空
void cderived::vfun1()
// 這裡僅僅是為了生成函式的彙編**,因此函式體為空
void cderived::vfun2()
// 這裡是為了分析物件的內部布局,因此僅僅是給成員變數賦值
void cderived::fun2()
m_ivalue1 = 13;
m_ivalue2 = 13;
現在用下面的**來呼叫成員函式:
cderived derived;
// 用物件呼叫虛函式
derived.vfun1();
derived.vfun2();
// 用物件呼叫非虛函式
derived.fun1();
derived.fun2();
// 用指向派生類的基類的指標呼叫虛函式,實現多型
cbase *ptest = &derived;
ptest->vfun1();
ptest->vfun2();
下面就是用vc2005編譯上面的**後生成的彙編**:
cderived derived;
0041195e lea ecx,[derived]
00411961 call cderived::cderived (411177h)
// **段1
derived.vfun1();
00411966 lea ecx,[derived]
00411969 call cderived::vfun1 (411078h)
derived.vfun2();
0041196e lea ecx,[derived]
00411971 call cderived::vfun2 (4111b8h)
derived.fun1();
00411976 lea ecx,[derived]
00411979 call cbase::fun1 (411249h)
derived.fun2();
0041197e lea ecx,[derived]
00411981 call cderived::fun2 (4111bdh)
// **段2
cbase *ptest = &derived;
00411986 lea eax,[derived]
00411989 mov dword ptr [ptest],eax
ptest->vfun1();
0041198c mov eax,dword ptr [ptest] // 行1
0041198f mov edx,dword ptr [eax] // 行2
00411991 mov esi,esp
00411993 mov ecx,dword ptr [ptest]
00411996 mov eax,dword ptr [edx] // 行3
00411998 call eax // 行4
0041199a cmp esi,esp
0041199c call @ilt+495(__rtc_checkesp) (4111f4h)
ptest->vfun2();
004119a1 mov eax,dword ptr [ptest]
004119a4 mov edx,dword ptr [eax]
004119a6 mov esi,esp
004119a8 mov ecx,dword ptr [ptest]
004119ab mov eax,dword ptr [edx+4] // 行5
004119ae call eax
004119b0 cmp esi,esp
004119b2 call @ilt+495(__rtc_checkesp) (4111f4h)
通過對**段1的觀察我們可以發現:通過物件呼叫類的虛成員函式和呼叫非虛成員函式
是相同的(對呼叫成員函式的彙編**的分析可以看我的那篇《**c++中的this指標》
)。也就是說,用物件是無法實現多型的。
下面主要來分析實現多型的**段2。
行1、將ptest指標指向的位址前2個字(4個位元組,也就是32位系統中乙個指標的大小)
的內容當成乙個指標放到eax暫存器中
行2、將eax暫存器中的指標的值放入edx暫存器
行3、將dex暫存器中的指標的值放入eax暫存器
行4、呼叫eax暫存器指向的函式
這樣分析似乎對怎樣呼叫物件derived的虛函式vfun1()並不是很清楚。那麼我們先來看
下面的這張圖:
這張圖是乙個假設的物件derived在記憶體中的內部布局圖。指標ptest指向物件derived,
而物件derived的前4個位元組是乙個虛表指標,指向虛函式表。
看著這張圖再來分析上面的彙編**就會清晰很多:
行1、取得虛表指標值放入eax暫存器中
行2、取得虛表指標的值放入edx暫存器中
行3、取得虛表指標指向的位址的值(也就是vfun1)放入eax暫存器中
行4、呼叫eax暫存器指向的函式
行5證明了上面圖中對虛函式表的假設。第二個虛函式vfun2()的位址就是通過在第一虛
函式vfun1()的位址加4(32位系統中乙個指標的大小)而得到的。
通過上面的分析,可以得出c++中虛函式的呼叫方法:首先,取得物件中的虛表指標;然
後,通過虛表指標找到相應的虛表;最後,通過在虛表內的偏移量找到相應的函式來調
用。上面的是**,對於虛函式有乙個基本的認識,下面開始具體的理論的講解。
如果不是虛函式,那麼我們物件一旦確定,我們的物件裡面的成員函式的指標就確定下來了,沒有必要需要額外的空間去存放函式指標。
如果乙個函式宣告為虛函式,那麼這個函式最終的實現是未定的,那麼就必須要在每乙個物件空間裡面放這樣的一塊空間來存放函式指標。這就是我們通常說的虛函式表。
上圖就是我們父物件與子物件的存放規則。在記憶體上面是鄰近的。對於乙個32位的系統,如果乙個類有虛函式,那麼在這個類的開始4個位元組就存放的虛函式表存放的位址。
還有乙個切片的問題,把乙個包含有虛函式的類作為指標傳遞的時候,在傳遞引數的時候,就對類進行了剪下,必須是按照指標或者引用來傳遞,而不是值的傳遞。
在c++中很少看到函式引數是通過值來傳遞的。如果按照值來傳遞,那麼我們的之類的虛函式就會被剪下掉。
虛析構函式:
因為我們析構函式,如果不為虛函式。我們delete 乙個基類的指標的時候,他只會呼叫這個積累的析構,但是如果這個指標指向的其實是我們的乙個具體的派生類,那麼具體派生類就不會呼叫其析構,導致**的問題。
一般來說,如果你的類中任何乙個函式是虛函式,那麼你的析構就要為虛。
虛函式,到呼叫乙個函式為虛的時候,這個時候系統做的事情是首先查詢這個虛函式表,看這兒函式又沒有對映到其他的地方。
虛指標 虛表及記憶體布局
首先要清楚,所謂指標其實質就是乙個記憶體位址值,形如0x12345678 其次,要知道,函式名本身就是乙個位址 虛指標 其實就是乙個位址值,以該位址為起始位址的一片記憶體單元存放著各虛函式的入口位址,這一片記憶體單元合起來就稱為虛函式表 想象一下 一片記憶體單元存著許多函式位址,想執行哪個虛函式就來...
C 虛函式在記憶體中的實現
首先來一張圖,一目了然 然後把相應的 貼上來 1 classa2 1011class b publica12 1920class c publicb21 後記 1 每個類的一開始都是乙個虛函式指標,這個指標指向乙個虛函式表,表中的每一項都是相應的虛函式的指標。2 類在繼承的時候,乙個子類的開始就是基...
C 中虛函式 虛表 虛指標例項講解
虛函式在c 中的實現機制就是用虛表和虛指標,但是具體是怎樣的呢?從more effecive c 其中一篇文章裡面可以知道 是每個類用了乙個虛表,每個類的物件用了乙個虛指標。具體的用法如下 class a class b public a a,b的實現省略 因為a有virtual void f 和g...