浅析C++继承的内存布局

代码如下

class Base
{
  public :
   Base()
   {
      printf("Base");
   }
  ~Base()
   {
      printf("~Base");
   }
  void SetNumber(int number) 
  {
    m_nBase=number;
  }
  int GetNumber()
  {
     return m_nBase;
   }
 private:
   int m_nBase;
};
class Derived:public  Base 
{
  public:
   void ShowNumber(int number)
   {
     SetNumber(number);
     m_nDerived=number+1;
     printf("%d",GetNumber());
    printf("%d",m_nDerived);
   
  }
int m_nDerived;


};
int main()
{


   Derived deriv;
    deriv.ShowNumber(5);
   return 0;
}


通过反汇编初步结果为

首先我们先看主函数,

00401070   push        ebp
00401071   mov         ebp,esp
00401073   push        0FFh
00401075   push        offset __ehhandler$_main (0041f279)
0040107A   mov         eax,fs:[00000000]
00401080   push        eax
00401081   mov         dword ptr fs:[0],esp
00401088   sub         esp,4Ch
0040108B   push        ebx
0040108C   push        esi
0040108D   push        edi
0040108E   lea         edi,[ebp-58h]
00401091   mov         ecx,13h
00401096   mov         eax,0CCCCCCCCh
0040109B   rep stos    dword ptr [edi]

这里的栈初始化有点不一样,规定基址ebp后,却压入了三个DWORD,也就是FFh, offset __ehhandler$_main (0041f279),还有eax,这点要注意。

  Derived deriv;
0040109D   lea         ecx,[ebp-14h]
004010A0   call        @ILT+45(Derived::Derived) (00401032)
004010A5   mov         dword ptr [ebp-4],0

往下走,就是子类的构造函数,00401032是个跳转语句,跳转到了

Derived::Derived:
00401220   push        ebp
00401221   mov         ebp,esp
00401223   sub         esp,44h
00401226   push        ebx
00401227   push        esi
00401228   push        edi
00401229   push        ecx
0040122A   lea         edi,[ebp-44h]
0040122D   mov         ecx,11h
00401232   mov         eax,0CCCCCCCCh
00401237   rep stos    dword ptr [edi]
00401239   pop         ecx
0040123A   mov         dword ptr [ebp-4],ecx
0040123D   mov         ecx,dword ptr [ebp-4]
00401240   call        @ILT+15(Base::Base) (00401014)
00401245   mov         eax,dword ptr [ebp-4]
00401248   pop         edi
00401249   pop         esi
0040124A   pop         ebx
0040124B   add         esp,44h
0040124E   cmp         ebp,esp
00401250   call        __chkesp (00408810)
00401255   mov         esp,ebp
00401257   pop         ebp

这里面我们要插入一句,C++里面总是说什么this指针,指向的是本对象的地址,这样讲有点云里雾里,不如我们用汇编来分析

00401088   sub         esp,4Ch

这一句代码是主函数的,意思是申请一个大小为0x4C的栈空间,栈空间存放的是子类对象。既然有了子类对象,如何初始化呢

0040109D   lea         ecx,[ebp-14h]
004010A0   call        @ILT+45(Derived::Derived) (00401032)

这里是讲ebp-14处的地址,其实也就是ebp-14h这个值,付给ecx,然后调用子类构造函数,为什么是ebp-14h呢,因为前面压入了三个双字变量,这里子类对象大小我们知道是父类大小+新增成员变量大小,为8,所以子类的对象的起始地址为ebp-14h(这里的14h,换成十进制,就是20)

图如下


           
   
                     
                             44h的空间(填充了0xCC)

 子类对象首地址   (ebp-14h) this

(这里其实存放的是从父类继承的m_nBase)

 子类对象首地址+4 ebp-10h) this+4

(这里存储的是子类新增的m_nDerived) 
          红色的两个框,8个字节,就是子类对象

 压入的第三个双字(ebp-C

 压入的第二个双字(ebp-8

 压入的第一个双字(ebp-4

 ebp

     
本图从下往上,是栈的增长方向。


我们看主函数这一句,0040109D   lea         ecx,[ebp-14h],意思是把地址为ebp-14h处的单元的地址付给ecx,也就是ebp-14h付给ecx,ebp-14h是子类的首地址,连接往下四个字节,一共八个字节,是子类的总大小。

然后进入 004010A0   call        @ILT+45(Derived::Derived) (00401032)

在子类的构造函数中我们可以看到,

00401239   pop         ecx
0040123A   mov         dword ptr [ebp-4],ecx
0040123D   mov         ecx,dword ptr [ebp-4]
00401240   call        @ILT+15(Base::Base) (00401014)

将传入的子类的首地址用ecx传入,在弹出,在保存在ebp-4h处,再传给ecx,并调用基类的构造函数。

我们这里的ecx,实际上存储的就是传说中的this指针了,也就是子类对象的首地址,通过传递这个地址,我们可以方便的对子类进行操作,比如对其成员变量进行赋值。

这一点算是分析完毕了,跳回去,我们接着分析子类构造函数。

看这句

00401240   call        @ILT+15(Base::Base) (00401014)

这个意思是调用父类的构造函数,明显,此时ecx存储的还是子类传过来的地址

  Base()
00401270   push        ebp
00401271   mov         ebp,esp
00401273   sub         esp,44h
00401276   push        ebx
00401277   push        esi
00401278   push        edi
00401279   push        ecx
0040127A   lea         edi,[ebp-44h]
0040127D   mov         ecx,11h
00401282   mov         eax,0CCCCCCCCh
00401287   rep stos    dword ptr [edi]
00401289   pop         ecx
0040128A   mov         dword ptr [ebp-4],ecx
19:      {
20:         printf("Base");
0040128D   push        offset string "Base" (00431020)
00401292   call        printf (00408940)
00401297   add         esp,4
21:      }

这个就是我们在构造函数中调用的父类构造函数,可以看到这里面打印了一个字符串,并没有进行赋值什么的,如果进行赋值呢?比如Base构造函数里把从父类继承过来的m_nBase=2,这样应该会变为,mov [ecx],2

有兴趣的可以修改下源程序,然后看下这里的反汇编代码。

到这里,子类的构造函数就分析完了,我们接着分析

 deriv.ShowNumber(5);
  void ShowNumber(int number)
41:      {
00401110   push        ebp
00401111   mov         ebp,esp
00401113   sub         esp,44h
00401116   push        ebx
00401117   push        esi
00401118   push        edi
00401119   push        ecx
0040111A   lea         edi,[ebp-44h]
0040111D   mov         ecx,11h
00401122   mov         eax,0CCCCCCCCh
00401127   rep stos    dword ptr [edi]
00401129   pop         ecx
0040112A   mov         dword ptr [ebp-4],ecx
42:        SetNumber(number);
0040112D   mov         eax,dword ptr [ebp+8]
00401130   push        eax
00401131   mov         ecx,dword ptr [ebp-4]
00401134   call        @ILT+10(Base::SetNumber) (0040100f)
43:        m_nDerived=number+1;
00401139   mov         ecx,dword ptr [ebp+8]
0040113C   add         ecx,1
0040113F   mov         edx,dword ptr [ebp-4]
00401142   mov         dword ptr [edx+4],ecx
44:        printf("%d",GetNumber());
00401145   mov         ecx,dword ptr [ebp-4]
00401148   call        @ILT+35(Base::GetNumber) (00401028)
0040114D   push        eax
0040114E   push        offset string "%d" (0043101c)
00401153   call        printf (00408940)
00401158   add         esp,8
45:       printf("%d",m_nDerived);
0040115B   mov         eax,dword ptr [ebp-4]
0040115E   mov         ecx,dword ptr [eax+4]
00401161   push        ecx
00401162   push        offset string "%d" (0043101c)
00401167   call        printf (00408940)
0040116C   add         esp,8
46:
47:     }


可以看到里面又调用了继承自父类的Setnumber函数,这里面还是用的ebp-14h进行传递,不多讲,SetNumber执行完后,ebp-14h这个地方,就变成了5,我们可以知道,这个双字,实际上是继承的父类的成员变量。

往下走,就是

 m_nDerived=number+1;
00401139   mov         ecx,dword ptr [ebp+8]
0040113C   add         ecx,1
0040113F   mov         edx,dword ptr [ebp-4]
00401142   mov         dword ptr [edx+4],ecx

这里可以看到,首先将形参自增1,变成6,然后

 mov         edx,dword ptr [ebp-4]
将首地址付给edx寄存器,然后将6,付给了edx+4处的内存,edx+4处的内存是哪里?我们看图,就知道,是首地址往下四个字节,也就是ebp-10h,也就是this+4处,也就是子类新增的成员变量的位置。
后面就基本结束了。
反汇编确实是利器,剖开C++晦涩的语法糖,直窥本源。



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
c variant是一种数据类型,它类似于Qt的QVariant类和Boost的any类。它可以将常见的数据类型封装在一个类中,以便在使用容器如std::vector时,能够存储不同类型的数据。引用中的例子展示了如何使用c variant,通过将不同类型的数据添加到std::vector<Variant>中来实现。例如,可以向vec中添加整数、字符串、布尔值和浮点数。这使得我们能够在一个容器中存储和处理不同类型的数据。 引用提供了一个简单的测试程序,展示了如何使用c variant。该程序包含了对Variant类的引用,并演示了如何创建一个std::vector<Variant>并添加不同类型的数据。然后,通过循环遍历vec中的元素并调用toString()函数,将每个元素打印到控制台。最后,使用system("pause")等待用户按下任意键结束程序。 总之,c variant是一种用于封装不同类型数据的数据类型,它可以方便地处理和存储各种类型的数据。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [C++ 实现Variant类](https://blog.csdn.net/WU9797/article/details/96768653)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *3* [浅析C++中boost.variant的几种访问方法](https://download.csdn.net/download/weixin_38622475/12791752)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值