C++单重继承分析

单重继承-无虚函数

测试代码如下:

class CBase
{
  int m_nTest;
public:
  CBase():m_nTest(0)
  {
    std::cout << "CBase()" << std::endl;
  }

  ~CBase()
  {
    std::cout << "~CBase()" << std::endl;
  }

  void ShowInfo1() { std::cout << m_nTest + 1 << std::endl; }
  void ShowInfo() { std::cout << m_nTest << std::endl; }
  void ShowInfo2() { std::cout << m_nTest + 2 << std::endl; }
};

class CDerived:public CBase
{
  int m_nTest1;
public:
  CDerived() :m_nTest1(1)
  {
    std::cout << "CDerived()" << std::endl;
  }

  ~CDerived()
  {
    std::cout << "~CDerived()" << std::endl;
  }
};


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

类对象的内存结构如下:
20190801101156.png

单重继承-有虚函数

  • 基类中有虚函数,派生类中无新增虚函数,派生类中没有重写父类的虚函数
    测试代码如下:

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

    CDerived继承自CBase,所以在CDerived类对象构造的时候先调用CBase的构造函数,此时CBase的构造函数
    会CBase类的虚表指针复制到CDerived对象存放虚表指针的位置(对象开始的四个字节),过程如下:
    20190801135340.png
    可以看出CBase类的数据成员m_nTest还未初始化,所以父类覆盖虚表指针的时间是在类数据成员初始化之前
    CBase类构造函数执行完成后,CDerived的构造函数开始执行:
    20190801135711.png
    通过对比可以发现CDerived类的构造函数执行时再次覆盖了虚表指针,当前的虚表指针指向了CDerived类
    自己的虚表;虽然CDerived类继承了CBase类,但是CDerived类并没有重写父类的虚函数,所以VC++编译器
    将CBase的虚表复制给了CDerived类的虚表

  • 基类中有虚函数,派生类中无新增虚函数,派生类中重写了父类的虚函数
    在Derived类中重写两个父类的虚函数:

     class CDerived:public CBase
     {
       int m_nTest1;
     public:
       CDerived();
       ~CDerived();
       virtual void ShowInfo1();
       virtual void ShowInfo();
     };
    
     void CDerived::ShowInfo1()
     {
       std::cout << " CDerived::ShowIfno1:" << m_nTest1 << std::endl;
     }
    
     void CDerived::ShowInfo()
     {
       std::cout << " CDerived::ShowInfo:" << m_nTest1 << std::endl;
     }
    
     CDerived::CDerived() :m_nTest1(1)
     {
       std::cout << "CDerived()" << std::endl;
     }
    
     CDerived::~CDerived()
     {
       std::cout << "~CDerived()" << std::endl;
     }
    

    观察CDerived对象构造过程中,虚表变化:
    CBase类的构造函数执行时虚表和虚表指针的变化:
    20190801141612.png
    在CBase类的构造函数执行过程中,对象的虚表指针始终指向CBase类的虚表
    CDerived类的构造函数执行时虚表和虚表指针的变化:
    20190801142422.png
    因为CDerived类重写了前两个虚函数,所以CDerived类的虚表前两个函数指针使用的CDerived自己的虚函
    数地址来填充,第三个虚函数因为没有重写,所以依旧使用CBase实现的虚函数的地址填充
     
    接下来观察下CDervied对象析构的过程:
    20190801143051.png
    可以看出在CDervied的析构函数执行的过程中虚表和虚表指针没有发生变化,看看其反汇编代码:

     CDerived::~CDerived()
     {
     000A2360 55                   push        ebp  
     000A2361 8B EC                mov         ebp,esp  
     000A2363 6A FF                push        0FFFFFFFFh  
     000A2365 68 B0 6F 0A 00       push        0A6FB0h  
     000A236A 64 A1 00 00 00 00    mov         eax,dword ptr fs:[00000000h]  
     000A2370 50                   push        eax  
     000A2371 81 EC CC 00 00 00    sub         esp,0CCh  
     000A2377 53                   push        ebx  
     000A2378 56                   push        esi  
     000A2379 57                   push        edi  
     000A237A 51                   push        ecx  
     000A237B 8D BD 28 FF FF FF    lea         edi,[ebp-0D8h]  
     000A2381 B9 33 00 00 00       mov         ecx,33h  
     000A2386 B8 CC CC CC CC       mov         eax,0CCCCCCCCh  
     000A238B F3 AB                rep stos    dword ptr es:[edi]  
     000A238D 59                   pop         ecx  
     000A238E A1 04 C0 0A 00       mov         eax,dword ptr [__security_cookie (0AC004h)]  
     000A2393 33 C5                xor         eax,ebp  
     000A2395 50                   push        eax  
     000A2396 8D 45 F4             lea         eax,[ebp-0Ch]  
     000A2399 64 A3 00 00 00 00    mov         dword ptr fs:[00000000h],eax  
     000A239F 89 4D EC             mov         dword ptr [this],ecx  
     000A23A2 8B 45 EC             mov         eax,dword ptr [this]  
     000A23A5 C7 00 60 9B 0A 00    mov         dword ptr [eax],offset CDerived::`vftable' (0A9B60h)  
       std::cout << "~CDerived()" << std::endl;
     000A23AB 8B F4                mov         esi,esp  
     000A23AD 68 9B 10 0A 00       push        offset std::endl<char,std::char_traits<char> > (0A109Bh)  
     000A23B2 68 38 9D 0A 00       push        offset string "~CDerived()" (0A9D38h)  
     000A23B7 A1 98 D0 0A 00       mov         eax,dword ptr [_imp_?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A (0AD098h)]  
     000A23BC 50                   push        eax  
     000A23BD E8 E5 EF FF FF       call        std::operator<<<std::char_traits<char> > (0A13A7h)  
     000A23C2 83 C4 08             add         esp,8  
     000A23C5 8B C8                mov         ecx,eax  
     000A23C7 FF 15 A8 D0 0A 00    call        dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (0AD0A8h)]  
     000A23CD 3B F4                cmp         esi,esp  
     000A23CF E8 9E ED FF FF       call        __RTC_CheckEsp (0A1172h)  
     }
     000A23D4 8B 4D EC             mov         ecx,dword ptr [this]  
     000A23D7 E8 3F EF FF FF       call        CBase::~CBase (0A131Bh)  
     000A23DC 8B 4D F4             mov         ecx,dword ptr [ebp-0Ch]  
     000A23DF 64 89 0D 00 00 00 00 mov         dword ptr fs:[0],ecx  
     000A23E6 59                   pop         ecx  
     000A23E7 5F                   pop         edi  
     000A23E8 5E                   pop         esi  
     000A23E9 5B                   pop         ebx  
     000A23EA 81 C4 D8 00 00 00    add         esp,0D8h  
     000A23F0 3B EC                cmp         ebp,esp  
     000A23F2 E8 7B ED FF FF       call        __RTC_CheckEsp (0A1172h)  
     000A23F7 8B E5                mov         esp,ebp  
     000A23F9 5D                   pop         ebp  
     000A23FA C3                   ret  
    

    但是从这句汇编代码:

     000A23A5 C7 00 60 9B 0A 00  mov dword ptr [eax],offset CDerived::`vftable' (0A9B60h)  
    

    可以看出CDerived类的析构函数在执行时还是做了一次虚表指针覆盖动作,用CDerived的虚表地址赋值给
    对象的虚表指针,但在CDerived类的构造函数执行前对象的虚表指针本来就指向CDerived的虚表,所以感觉
    VC++编译器生成的这句代码是多余的,有可能是因为当前程序版本是dubug版本,为了方便调试就多生成了一
    些代码。
    CDerived类的析构函数执行完成后,在CBase类的析构函数开始执行:
    20190801144506.png
    此时对象的虚表指针被CBase的析构函数置为指向CBase类虚表

  • 基类中有虚函数,派生类中有新增虚函数,并且派生类中重写父类的虚函数
    CDerived代码如下:

     class CDerived:public CBase
     {
       int m_nTest1;
     public:
       CDerived();
       ~CDerived();
       virtual void NewVirtualFuction1();
       virtual void ShowInfo1();
       virtual void ShowInfo();
       virtual void NewVirtualFuction2();
     };
    
    
     void CDerived::NewVirtualFuction1()
     {
       std::cout << " CDerived::NewVirtualFuction:" << m_nTest1 << std::endl;
     }
    
     void CDerived::NewVirtualFuction2()
     {
       std::cout << " CDerived::NewVirtualFuction:" << m_nTest1 << std::endl;
     }
    
    
     void CDerived::ShowInfo1()
     {
       std::cout << " CDerived::ShowIfno1:" << m_nTest1 << std::endl;
     }
    
     void CDerived::ShowInfo()
     {
       std::cout << " CDerived::ShowInfo:" << m_nTest1 << std::endl;
     }
    
     CDerived::CDerived() :m_nTest1(1)
     {
       std::cout << "CDerived()" << std::endl;
     }
    
     CDerived:: ~CDerived()
     {
       std::cout << "~CDerived()" << std::endl;
     }
    

    先是CBase类的构造函数进行虚表指针覆盖:
    20190801154143.png
    然后CDerived类的构造函数进行虚表指针的覆盖:
    20190801154502.png
    20190801154700.png
    可以VC++编译器在为CDerived类生成虚表时,先将父类的虚表复制一份,如果子类重写了父类的某个虚函数,
    就用子类重写的虚函数的地址覆盖虚表中的对应位置,子类自己新增的虚函数则全部追加到这个副本的尾部,
    最终形成了子类自己的虚表

虚析构

代码如下:

class CBase
{
  int m_nTest;
public:
  CBase();
  virtual ~CBase();
};

CBase::CBase() :m_nTest(0)
{
  std::cout << "CBase()" << std::endl;
}

CBase::~CBase()
{
  std::cout << "~CBase()" << std::endl;
}


class CDerived:public CBase
{
  int m_nTest1;
public:
  CDerived();
  ~CDerived();
};

CDerived::CDerived() :m_nTest1(1)
{
  std::cout << "CDerived()" << std::endl;
}

CDerived::~CDerived()
{
  std::cout << "~CDerived()" << std::endl; 
}

int main(int argc, char* argv[])
{
  CBase * p = new CDerived;
  delete p;
  return 0;
}

运行结果:
20190801165701.png
再来看看其反汇编代码:

delete p;
000D6649 89 85 FC FE FF FF    mov         dword ptr [ebp-104h],eax  
000D664F 8B 8D FC FE FF FF    mov         ecx,dword ptr [ebp-104h]  
000D6655 89 8D 08 FF FF FF    mov         dword ptr [ebp-0F8h],ecx  
000D665B 83 BD 08 FF FF FF 00 cmp         dword ptr [ebp-0F8h],0  
000D6662 74 15                je          main+0C9h (0D6679h)  
000D6664 6A 01                push        1  
000D6666 8B 8D 08 FF FF FF    mov         ecx,dword ptr [ebp-0F8h]  

000D666C E8 3A AE FF FF       call        CBase::`scalar deleting destructor' (0D14ABh)  

000D6671 89 85 F4 FE FF FF    mov         dword ptr [ebp-10Ch],eax  
000D6677 EB 0A                jmp         main+0D3h (0D6683h)  
000D6679 C7 85 F4 FE FF FF 00 00 00 00 mov         dword ptr [ebp-10Ch],0  

可以看出当用一个基类指针指向动态分配的子类类对象,然后直接通过这个基类指针释放对象,VC++编译器生成
的代码只调用了基类的析构函数,函数,子类的析构函数并没有被调用,如果子类对象中引用了其它资源,就造成
了资源泄漏;所以如果一个类作为基类,那么其虚函数必须得是虚函数,否则当使用一个基类指针指向一个动态分
配的子类对象,然后通过这个基类指针去释放动态分配的子类对象时,子类的析构函数会被调用.

 
现在将基类的析构函数声明前加virtual关键字,使其成为虚析构函数,观察构造过程,以及通过基类指针释放
动态分配的子类对象时的过程:
20190801171527.png
20190801171643.png
从这里可以看出析构函数是一个特殊的成员函数,如果一个类的析构函数为虚函数,那么继承它的子类的析构函
数也是虚函数.

 
在看看此时的反汇编代码:

delete p;
012429B6 8B 45 EC             mov         eax,dword ptr [p]         //取出虚表指针
012429B9 89 85 FC FE FF FF    mov         dword ptr [ebp-104h],eax  
012429BF 8B 8D FC FE FF FF    mov         ecx,dword ptr [ebp-104h]  
012429C5 89 8D 08 FF FF FF    mov         dword ptr [ebp-0F8h],ecx  
012429CB 83 BD 08 FF FF FF 00 cmp         dword ptr [ebp-0F8h],0  
012429D2 74 25                je          main+0D9h (012429F9h)  
012429D4 8B F4                mov         esi,esp  
012429D6 6A 01                push        1  
012429D8 8B 95 08 FF FF FF    mov         edx,dword ptr [ebp-0F8h]  
012429DE 8B 02                mov         eax,dword ptr [edx]  
012429E0 8B 8D 08 FF FF FF    mov         ecx,dword ptr [ebp-0F8h]  
012429E6 8B 10                mov         edx,dword ptr [eax]  
012429E8 FF D2                call        edx                    //调用CDerived类的析构
012429EA 3B F4                cmp         esi,esp  
012429EC E8 A4 E7 FF FF       call        __RTC_CheckEsp (01241195h)  
012429F1 89 85 F4 FE FF FF    mov         dword ptr [ebp-10Ch],eax  
012429F7 EB 0A                jmp         main+0E3h (01242A03h)  
012429F9 C7 85 F4 FE FF FF 00 00 00 00 mov         dword ptr [ebp-10Ch],0  

上面代码中只有析构函数是虚函数,所以析构函数的指针位于虚表中的第一项,所以上面汇编代码中的:

012429E6 8B 10                mov         edx,dword ptr [eax]  
012429E8 FF D2                call        edx 

通过虚表调用到CDerived类的析构函数,因为CDerived类继承自CBase类,按照C++标准,子类析构完成后在调用
父类的析构函数,所以VC++编译器直接调用CBase的析构函数的代码插入到CDerived类析构函数的后面,再看下
CDerived类的析构函数的反汇编代码:

CDerived::~CDerived()
{
00F62450 55                   push        ebp  
00F62451 8B EC                mov         ebp,esp  
00F62453 6A FF                push        0FFFFFFFFh  
00F62455 68 F0 73 F6 00       push        0F673F0h  
00F6245A 64 A1 00 00 00 00    mov         eax,dword ptr fs:[00000000h]  
00F62460 50                   push        eax  
00F62461 81 EC CC 00 00 00    sub         esp,0CCh  
00F62467 53                   push        ebx  
00F62468 56                   push        esi  
00F62469 57                   push        edi  
00F6246A 51                   push        ecx  
00F6246B 8D BD 28 FF FF FF    lea         edi,[ebp-0D8h]  
00F62471 B9 33 00 00 00       mov         ecx,33h  
00F62476 B8 CC CC CC CC       mov         eax,0CCCCCCCCh  
00F6247B F3 AB                rep stos    dword ptr es:[edi]  
00F6247D 59                   pop         ecx  
00F6247E A1 04 C0 F6 00       mov         eax,dword ptr [__security_cookie (0F6C004h)]  
00F62483 33 C5                xor         eax,ebp  
00F62485 50                   push        eax  
00F62486 8D 45 F4             lea         eax,[ebp-0Ch]  
00F62489 64 A3 00 00 00 00    mov         dword ptr fs:[00000000h],eax  
00F6248F 89 4D EC             mov         dword ptr [this],ecx  
00F62492 8B 45 EC             mov         eax,dword ptr [this]  
00F62495 C7 00 54 9B F6 00    mov         dword ptr [eax],offset CDerived::`vftable' (0F69B54h)  
  std::cout << "~CDerived()" << std::endl; 
00F6249B 8B F4                mov         esi,esp  
00F6249D 68 96 10 F6 00       push        offset std::endl<char,std::char_traits<char> > (0F61096h)  
00F624A2 68 64 9B F6 00       push        offset string "~CDerived()" (0F69B64h)  
00F624A7 A1 98 D0 F6 00       mov         eax,dword ptr [_imp_?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A (0F6D098h)]  
00F624AC 50                   push        eax  
00F624AD E8 59 EF FF FF       call        std::operator<<<std::char_traits<char> > (0F6140Bh)  
00F624B2 83 C4 08             add         esp,8  
00F624B5 8B C8                mov         ecx,eax  
00F624B7 FF 15 A4 D0 F6 00    call        dword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (0F6D0A4h)]  
00F624BD 3B F4                cmp         esi,esp  
00F624BF E8 D1 EC FF FF       call        __RTC_CheckEsp (0F61195h)  
}

/**********VC++编译器将调用CBase的代码偷偷插入到CDerived析构函数的尾部***************/
00F624C4 8B 4D EC             mov         ecx,dword ptr [this]  
00F624C7 E8 02 F0 FF FF       call        CBase::~CBase (0F614CEh)  
/********************************************************************************/
00F624CC 8B 4D F4             mov         ecx,dword ptr [ebp-0Ch]  
00F624CF 64 89 0D 00 00 00 00 mov         dword ptr fs:[0],ecx  
00F624D6 59                   pop         ecx  
00F624D7 5F                   pop         edi  
00F624D8 5E                   pop         esi  
00F624D9 5B                   pop         ebx  
00F624DA 81 C4 D8 00 00 00    add         esp,0D8h  
00F624E0 3B EC                cmp         ebp,esp  
00F624E2 E8 AE EC FF FF       call        __RTC_CheckEsp (0F61195h)  
00F624E7 8B E5                mov         esp,ebp  
00F624E9 5D                   pop         ebp  
00F624EA C3                   ret  

总结:

  • 如果一个类有虚函数,那么这个VC++编译器一定会为其生成虚表,所有对象共用这一个虚表
  • 调用父类构造函数和析构函数期间对象的虚表指针指向父类的虚表,调用子类构造函数后虚表指针指向
    子类自己的虚表,构造和析构期间都会做虚表指针的覆盖动作
  • 一个类如果要作为基类,那么其析构函数一定得是虚函数,以免使用基类指针或者应用释放指向子类对象时
    造成资源泄漏
  • 析构函数是一个特殊的成员函数,如果父类的析构函数是虚函数,那么子类的析构函数也是虚函数
  • 无论是VC++编译器还是其它的C++编译器,为了实现C++标准定义的行为,都会偷偷生成代码
原文地址:https://www.cnblogs.com/UnknowCodeMaker/p/11284828.html