C++虛函數


 

C++虛函數:

  • 僅在定義父類成員函數的函數原型前加關鍵字virtual,子類如果重寫了父類的虛函數那么子類前的virtual
    關鍵字可寫可不寫,但是為了代碼具有可讀性,最好還是加上virtual關鍵字。
  • 子類重寫父類虛函數的條件:
    子類的函數名稱與父類的虛函數名稱相同,參數列表也要相同,返回值也相同(如果返回的是子類類型的指針或
    者引用也可以),調用約定也相同,即使沒有寫virtual關鍵字,編譯器也視為重寫父類虛函數
  • 多態就是一種函數覆蓋:
    函數覆蓋:a.作用域不同
    b.函數名/參數列表/ 返回值/調用約定相同
    c.該函數必須為虛函數
     
    函數重載:a.作用域相同 b.函數名相同 c.參數列表相同(不考慮返回值和調用約定)
     
    數據隱藏:a.作用域不同 b.函數名稱相同

虛表指針:

  • VC++編譯器在編譯時發現如果一個類有虛函數,那么編譯器將會為這個類生成一個虛表(類似函數指針數組),
    並且VC++編譯器會在該類的第一個數據成員前插入一個指向該虛表的指針
    下面用簡單的代碼測下:

    class CTest
    {
      int m_nTest;
    public:
      CTest():m_nTest(1){}
      void virtual ShowInfo() { std::cout << m_nTest << std::endl; }
      void virtual ShowInfo1() { std::cout << m_nTest+1 << std::endl; }
      void virtual ShowInfo2() { std::cout << m_nTest+2 << std::endl; }
    };
    
    int main(int argc, char* argv[])
    {
      CTest t;
      t.ShowInfo();
      t.ShowInfo1();
      t.ShowInfo2();
      return 0;
    }
    

    讓程序停在這個斷點處:
    20190731160131.png
    在監視窗口中查看類對象t所在內存地址,並在內存窗口中查看t的內存布局:
    20190731162817.png
    可以看出對象t的起始地址為0x0048F730,但是這個地址存放的並不是數據成員m_nTest,其實VS的監視窗口已
    經將其解釋為_vfptr(虛表指針),_vfptr指針的值為0x001babdc,現在轉到這個地址處的內存:
    20190731164035.png
    這個三個指針分別對應虛函數ShowInfo,ShowInfo1,ShowInfo3,如下圖:
    20190731164312.png
    因為這是debug版的程序,所以會有jmp跳轉,方便調試,如果是Release版的程序將不會有這些jmp指令,
    調用時直接轉移到對應的函數中

  • 虛表中的虛函數順序與虛函數在類中的聲明位置有關,在類中第一個聲明的虛函數在虛表中的位置總是第一個
    第二個虛函數則排放在虛表中的第二個位置,依次排放
    下面做個測試,調整虛函數在類中的聲明位置,查看其在虛表中的位置變化:
    例:虛函數在類中聲明如下:

    class CTest
    {
      int m_nTest;
    public:
      CTest():m_nTest(1){}
    
      void virtual ShowInfo() { std::cout << m_nTest << std::endl; }
      void virtual ShowInfo1() { std::cout << m_nTest+1 << std::endl; }
      void virtual ShowInfo2() { std::cout << m_nTest+2 << std::endl; }
    };
    

    此時虛函數在虛表中的位置如下:
    20190731165633.png

     
    調整虛函數在類中的聲明位置如下:

    class CTest
    {
      int m_nTest;
    public:
      CTest():m_nTest(1){}
      void virtual ShowInfo1() { std::cout << m_nTest + 1 << std::endl; }
      void virtual ShowInfo() { std::cout << m_nTest << std::endl; }
      void virtual ShowInfo2() { std::cout << m_nTest+2 << std::endl; }
    };
    

    此時虛函數在虛表中的位置如下:
    20190731181225.png

    可以看出隨着虛函數在類中聲明位置的變化,虛函數在虛表中的位置也發生對應的改變

直接調用與間接調用(虛調用)

  • 通過類對象的方式調用虛函數稱為直接調用,編譯器直接生成調用該虛函數的代碼
    例:

    int main(int argc, char* argv[])
    {
      CTest t;
      t.ShowInfo();
      t.ShowInfo1();
      t.ShowInfo2();
      return 0;
    }
    

    觀察上面代碼的反匯編代碼:

    CTest t;
    008D1A58  lea         ecx,[t]  
    008D1A5B  call        CTest::CTest (08D1096h)  
      t.ShowInfo();
    008D1A60  lea         ecx,[t]  
    008D1A63  call        CTest::ShowInfo (08D10B4h)  
      t.ShowInfo1();
    008D1A68  lea         ecx,[t]  
    008D1A6B  call        CTest::ShowInfo1 (08D11B3h)  
      t.ShowInfo2();
    008D1A70  lea         ecx,[t]  
    008D1A73  call        CTest::ShowInfo2 (08D104Bh)  
      return 0;
    008D1A78  xor         eax,eax  
    

    可以看出VC++編譯器生成的直接調用對應虛函數的代碼

  • 通過指向對象的指針或引用調用虛函數,稱為間接調用,編譯器生成直接調用虛函數的代碼,而是通過虛表指針
    取出虛表內的虛函數指針,然后用虛函數指針調用虛函數

    例:

    int main(int argc, char* argv[])
    {
      CTest t;
      CTest & rt = t;
      CTest * pt = &t;
      rt.ShowInfo2();
      pt->ShowInfo();
      return 0;
    }
    

    觀察上面代碼的反匯編代碼:

      CTest t;
      00121A58  lea         ecx,[t]  
      00121A5B  call        CTest::CTest (0121096h)  
        CTest & rt = t;
      00121A60  lea         eax,[t]  
      00121A63  mov         dword ptr [rt],eax  
        CTest * pt = &t;
      00121A66  lea         eax,[t]  
      00121A69  mov         dword ptr [pt],eax  
        rt.ShowInfo2();
      00121A6C  mov         eax,dword ptr [rt]    //取對象t的地址
      00121A6F  mov         edx,dword ptr [eax]   //取虛表指針
      00121A71  mov         esi,esp  
      00121A73  mov         ecx,dword ptr [rt]  
      00121A76  mov         eax,dword ptr [edx+8] //取出ShowInfo2在虛表中的函數指針
      00121A79  call        eax                   //調用虛函數ShowInfo2
      00121A7B  cmp         esi,esp                
      00121A7D  call        __RTC_CheckEsp (0121140h)  
        pt->ShowInfo();
      00121A82  mov         eax,dword ptr [pt]    //取對象t的地址
      00121A85  mov         edx,dword ptr [eax]   //取虛表指針
      00121A87  mov         esi,esp  
      00121A89  mov         ecx,dword ptr [pt]    
      00121A8C  mov         eax,dword ptr [edx+4]  //取出ShowInfo在虛表中的函數指針
      00121A8F  call        eax                    //調用虛函數ShowInfo
      00121A91  cmp         esi,esp  
      00121A93  call        __RTC_CheckEsp (0121140h)  
        return 0;
      00121A98  xor         eax,eax  
    
  • 在普通成員函數中調用虛函數依然是間接調用
    例:在CTest類中加入一個如下的成員函數:

    void Test() 
    { 
      ShowInfo(); 
    }
    

    使用如下代碼測試:

    int main(int argc, char* argv[])
    {
      CTest t;
      t.Test();
      return 0;
    }
    

    Test函數對應的反匯編代碼如下:

    void Test() { ShowInfo(); }
    013719D0  push        ebp  
    013719D1  mov         ebp,esp  
    013719D3  sub         esp,0CCh  
    013719D9  push        ebx  
    013719DA  push        esi  
    013719DB  push        edi  
    013719DC  push        ecx  
    013719DD  lea         edi,[ebp-0CCh]  
    013719E3  mov         ecx,33h  
    013719E8  mov         eax,0CCCCCCCCh  
    013719ED  rep stos    dword ptr es:[edi]  
    013719EF  pop         ecx  
    013719F0  mov         dword ptr [this],ecx  
    013719F3  mov         eax,dword ptr [this]  
    013719F6  mov         edx,dword ptr [eax]  
    013719F8  mov         esi,esp  
    013719FA  mov         ecx,dword ptr [this]  //取虛表指針
    013719FD  mov         eax,dword ptr [edx+4] //從虛表中取虛函數ShowInfo的指針
    01371A00  call        eax                   //調用虛函數
    01371A02  cmp         esi,esp  
    01371A04  call        __RTC_CheckEsp (01371140h)  
    01371A09  pop         edi  
    01371A0A  pop         esi  
    01371A0B  pop         ebx  
    01371A0C  add         esp,0CCh  
    01371A12  cmp         ebp,esp  
    01371A14  call        __RTC_CheckEsp (01371140h)  
    01371A19  mov         esp,ebp  
    01371A1B  pop         ebp  
    01371A1C  ret
    

    可以看出在成員函數中調用虛函數是間接調用,也是根據虛表來調用

  • 在構造函數和析構函數中不會通過虛表來調用虛函數,而是直接在編譯時生成直接調用虛函數的代碼
    例:

    CTest():m_nTest(1)
    {
      ShowInfo1();
    }
    
    ~CTest()
    {
      ShowInfo1();
    }
    

    對應反匯編代碼如下:

    CTest():m_nTest(1)
    000D181C  mov         eax,dword ptr [this]  
    000D181F  mov         dword ptr [eax+4],1  
        ShowInfo1();
    000D1826  mov         ecx,dword ptr [this]  
    000D1829  call        CTest::ShowInfo1 (0D11C2h)  
    
     ~CTest()
    {
        000D1860  push        ebp  
        000D1861  mov         ebp,esp  
        000D1863  push        0FFFFFFFFh  
        000D1865  push        0D60D0h  
        000D186A  mov         eax,dword ptr fs:[00000000h]  
        000D1870  push        eax  
        000D1871  sub         esp,0CCh  
        000D1877  push        ebx  
        000D1878  push        esi  
        000D1879  push        edi  
        000D187A  push        ecx  
        000D187B  lea         edi,[ebp-0D8h]  
        000D1881  mov         ecx,33h  
        000D1886  mov         eax,0CCCCCCCCh  
        000D188B  rep stos    dword ptr es:[edi]  
        000D188D  pop         ecx  
        000D188E  mov         eax,dword ptr [__security_cookie (0DB004h)]  
        000D1893  xor         eax,ebp  
        000D1895  push        eax  
        000D1896  lea         eax,[ebp-0Ch]  
        000D1899  mov         dword ptr fs:[00000000h],eax  
        000D189F  mov         dword ptr [this],ecx  
        000D18A2  mov         eax,dword ptr [this]  
        000D18A5  mov         dword ptr [eax],offset CTest::`vftable' (0D8B34h)  
            ShowInfo1();
        000D18AB  mov         ecx,dword ptr [this]  
        000D18AE  call        CTest::ShowInfo1 (0D11C2h)   //直接調用
    }
    

構造和析構函數是否能為虛函數?

  • 構造函數不能為虛函數,因為如果對象都沒有創建,就無法調用虛函數,構造函數為虛函數是沒有任何意義的。
  • 析構函數可以是虛函數,在某些情況下必須為虛函數:當一個基類指針指向動態分配的子類對象時,這時如果 delete該基類指針,如果基類的析構函數不是虛函數,那么只會釋放基類自己的那部分,而派生類自己的那
    部分得不到釋放,這是不安全的,如果子類的數據成員部分有動態分配的資源,那么就發生了內存泄漏,但是
    可以將基類的析構函數定義為虛析構函數,這樣做即使是delete一個指向派生類對象的基類指針,也會先調用派生類的析構函數,在調用父類的析構函數。所以將析構函數設為虛函數總是正確的,后面會做實驗驗證

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM