• 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一个指向派生类对象的基类指针,也会先调用派生类的析构函数,在调用父类的析构函数。所以将析构函数设为虚函数总是正确的,后面会做实验验证
  • 相关阅读:
    仿IOS中下拉刷新的“雨滴”效果
    BZOJ 4216 Pig 分块乱搞
    mybatis学习笔记(10)-一对一查询
    关于人性,我是这么看的——“唯进化”论!
    IDEA引MAVEN项目jar包依赖导入问题解决
    IntelliJ IDEA 缓存和索引介绍和清理方法
    springboot整合mybatis使用阿里(阿里连接池)和xml方式
    Intellij 如何在新窗口中打开项目
    intellij idea 在什么地方打开终端Terminal
    Spring Boot 集成MyBatis
  • 原文地址:https://www.cnblogs.com/UnknowCodeMaker/p/11279021.html
Copyright © 2020-2023  润新知