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一个指向派生类对象的基类指针,也会先调用派生类的析构函数,在调用父类的析构函数。所以将析构函数设为虚函数总是正确的,后面会做实验验证
原文地址:https://www.cnblogs.com/UnknowCodeMaker/p/11279021.html