淺析C++中虛函數的調用及對象的內部布局

來源:互聯網
上載者:User

     在我那篇《淺析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++中虛函數的調用方法:首先,取得對象中的虛表指標;然後,通過虛表指標找到相應的虛表;最後,通過在虛表內的位移量找到相應的函數來調用。
    下面通過分析類CDerived的非虛成員函數Fun2()來證明上面圖中虛函數表指標的存在。

void CDerived::Fun2()
{
004118F0  push        ebp  
004118F1  mov         ebp,esp 
004118F3  sub         esp,0CCh 
004118F9  push        ebx  
004118FA  push        esi  
004118FB  push        edi  
004118FC  push        ecx  
004118FD  lea         edi,[ebp-0CCh] 
00411903  mov         ecx,33h 
00411908  mov         eax,0CCCCCCCCh 
0041190D  rep stos    dword ptr es:[edi] 
0041190F  pop         ecx  
00411910  mov         dword ptr [ebp-8],ecx 
    m_iValue1 = 13;
00411913  mov         eax,dword ptr [this] // 行6
00411916  mov         dword ptr [eax+4],0Dh // 行7
    m_iValue2 = 13;
0041191D  mov         eax,dword ptr [this] 
00411920  mov         dword ptr [eax+8],0Dh 
}
00411927  pop         edi  
00411928  pop         esi  
00411929  pop         ebx  
0041192A  mov         esp,ebp 
0041192C  pop         ebp  
0041192D  ret  

    上面是類CDerived的非虛成員函數Fun2()的彙編代碼。可以看到,行6是將this指向的地址放入eax寄存器,而行7是給this指標指向的地址加4的地址賦值(具體的分析,可以看《淺析C++中的this指標》),而這個地址裡面存放的是類CDerived的第一個成員變數。我們知道this指標是指向對象首地址的,那麼為什麼要給第一個成員變數賦值的時候要向後移動4個位元組?答案是因為對象的前4個位元組是用來存放虛表指標的。
    下面的代碼是《淺析C++中的this指標》一文中的不含虛函數的類的C++代碼和編譯後的彙編代碼:

class CTest
{
public:
    void SetValue();

private:
    int m_iValue1;
    int m_iValue2;
};

void CTest::SetValue()
{
    m_iValue1 = 13;
    m_iValue2 = 13;
}

void CTest::SetValue()
{
004117E0  push        ebp  
004117E1  mov         ebp,esp 
004117E3  sub         esp,0CCh 
004117E9  push        ebx  
004117EA  push        esi  
004117EB  push        edi  
004117EC  push        ecx  
004117ED  lea         edi,[ebp-0CCh] 
004117F3  mov         ecx,33h 
004117F8  mov         eax,0CCCCCCCCh 
004117FD  rep stos    dword ptr es:[edi] 
004117FF  pop         ecx  
00411800  mov         dword ptr [ebp-8],ecx 
    m_iValue1 = 13;
00411803  mov         eax,dword ptr [this] // 行8
00411806  mov         dword ptr [eax],0Dh // 行9
    m_iValue2 = 13;
0041180C  mov         eax,dword ptr [this] 
0041180F  mov         dword ptr [eax+4],0Dh 
}
00411816  pop         edi  
00411817  pop         esi  
00411818  pop         ebx  
00411819  mov         esp,ebp 
0041181B  pop         ebp  
0041181C  ret  

    通過行8、行9和行6、行7的比較就可以看出:類CTest的對象前4個位元組存放的是自己的第一個成員變數;而類CDerived的對象從第5個位元組開始才是存放的自己的第一個成員變數,它的前4個位元組是用來存放虛表指標的。這再一次證明了上面圖中對象內部布局的正確性。

PS:

    這篇文章可以說是《淺析C++中的this指標》的續篇,最後我說說我為什麼會用這種方法來分析C++,也算是對《淺析C++中的this指標》一文中網友評論的回複吧。
    dch4890164建議我看inside the c++ object model;而hacker47卻說了風涼話:“孔乙己說:回字有三種寫法,你們知道嗎?”;最直接的是wengch,直接反問我:“用彙編分析C++.....有意義麼?”。而我要說的是,《Inside The C++ Object Model》這本書我看過,確實是一本非常好的講解C++底層的書。可是由於平時寫C++代碼的時候,很少會關心底層的實現,所以那本書看過之後留下的印象並不深刻。而用彙編代碼來分析C++也是源於一個很偶然的事件:就是《淺析C++中的this指標》一文中提到的可以用一個類的null 指標來調用成員函數。我發現我的C++知識不能解釋那種現象,在Debug代碼的時候,我轉到了彙編代碼中來尋找答案。後來就把我的分析結果寫成了那篇《淺析C++中的this指標》。說實話,這也是我第一次接觸Windows下的組合語言,文章中的分析都是邊看資料邊揣摩得出的。也許會有人覺得我這種方法不值一提,但是我卻通過這種方法對C++的底層實現加深了瞭解。如果網友們看了覺得有收穫,那我就心滿意足了。呵呵~~

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.