zoukankan      html  css  js  c++  java
  • 对象内存布局 (11)

    注意:关于内存对齐(memory alignment),请看关于内存对齐问题,后面将会用到。

    下面我们进行在普通继承(即非虚继承)时,派生类的指针转换到基类指针的情形研究。假定各类之间的关系如下图:

    代码如下:

    #include <iostream>
    using namespace std;
    class Parent
    {
    public:
        int parent;
    };
    class Child : public Parent
    {
    public:
        int child;
    };
    class GrandChild : public Child
    {
    public:
        int grandchild;
    };
    int main(void)
    {
        Child* pc = new Child();
        GrandChild* pgc = new GrandChild();
        cout << "1. The address of Child object:		";
        cout << (unsigned long*)pc << endl;
        cout << "2. The address of GrandChild object:	";
        cout << (unsigned long*)pgc << endl;
        // 将类Child对象的指针pc,向上转型(upcast)到其父类型指针,即Parent*
        Parent* pp = (Parent*)pc;
        cout << "3. Child* casted to Parent*:		";
        cout << (unsigned long*)pp << endl;
        // 将类GrandChild对象的指针pgc,向上转型(upcast)到其父类型指针,即Child*
        Child* pc2 = (Child*)pgc;
        cout << "4. GrandChild* casted to Child*:	";
        cout << (unsigned long*)pc2 << endl;
        // 再将上面通过转型得到的类Child对象的指针pc2,向上转型(upcast)到其父类型指针,即Parent*
        Parent* pp2 = (Parent*)pc2;
        cout << "5. Child* casted to Parent*:		";
        cout << (unsigned long*)pp2 << endl;
        // 将类GrandChild对象的指针pgc,向上转型(upcast)到其祖类型指针,即Parent*
        Parent* pp3 = (Parent*)pgc;
        cout << "6. GrandChild* casted to Parent*:	";
        cout << (unsigned long*)pp3 << endl;
        return 0;
    }

    运行结果:

    我们发现在普通继承的情况下,将派生类对象的指针upcast为基类指针时,指针的值并不会发生改变。

    比如上面输出中的1和3是一样的。

             Child* pc = new Child();             ->               pc = 0x3d10e0

             Parent* pp = (Parent*)pc;           ->               pp = 0x3d10e0

    还有上面的2和4的输出也是一样的:

             GrandChild* pgc = new GrandChild();       ->          pgc = 0x3d10f0

             Child* pc2 = (Child*)pgc;                        ->          pc2 = 0x3d10f0

    Grandchild的内存分布图:

    保持整个程序其他部分代码不做任何变动,我们将Child改为从Parent虚继承,改后的Child代码如下:

    #include <iostream>
    using namespace std;
    class Parent
    {
    public:
        int parent;
    };
    class Child : public virtual Parent
    {
    public:
        int child;
    };
    class GrandChild : public Child
    {
    public:
        int grandchild;
    };
    int main(void)
    {
        Child* pc = new Child();
        GrandChild* pgc = new GrandChild();
        cout << "1. The address of Child object:		";
        cout << (unsigned long*)pc << endl;
        cout << "2. The address of GrandChild object:	";
        cout << (unsigned long*)pgc << endl;
        // 将类Child对象的指针pc,向上转型(upcast)到其父类型指针,即Parent*
        Parent* pp = (Parent*)pc;
        cout << "3. Child* casted to Parent*:		";
        cout << (unsigned long*)pp << endl;
        // 将类GrandChild对象的指针pgc,向上转型(upcast)到其父类型指针,即Child*
        Child* pc2 = (Child*)pgc;
        cout << "4. GrandChild* casted to Child*:	";
        cout << (unsigned long*)pc2 << endl;
        // 再将上面通过转型得到的类Child对象的指针pc2,向上转型(upcast)到其父类型指针,即Parent*
        Parent* pp2 = (Parent*)pc2;
        cout << "5. Child* casted to Parent*:		";
        cout << (unsigned long*)pp2 << endl;
        // 将类GrandChild对象的指针pgc,向上转型(upcast)到其祖类型指针,即Parent*
        Parent* pp3 = (Parent*)pgc;
        cout << "6. GrandChild* casted to Parent*:	";
        cout << (unsigned long*)pp3 << endl;
        return 0;
    }

    运行结果:

    与上图相比较

    现在来一一比较两者之间的不同:

    第1条,两者相同;

    第2条,两者相同;

    第3条,由0x7d10e0变成了0x7d10e8了,也就是说经过Parent* pp = (Parent*)pc;后,即由pc = 0x7d10e0得到了pp = 0x7d10e8。很奇怪!

    第4条,两者相同;

    第5条,由0x7d10f8变成了0x7d1104了,也就是说经过Parent* pp2 = (Parent*)pc2;后,即由pc2 = 0x7d10f8得到了pp2 = 0x7d1104。很奇怪!

    第6条,由0x7d10f8变成了0x7d1104了,也就是说经过Parent* pp3 = (Parent*)pgc;后,即由pgc = 0x7d10f8得到了pp3 = 0x7d1104。很奇怪!

    为什么会这样呢?通过上述分析发现,出现这种指针发生变化的情况,均发生在将Child*或者GrandChild*转换到Parent*的各行。Parent是Child的虚基类,Child又是GrandChild的基类。在上面的第4条中,我们通过Child* pc2 = (Child*)pgc;,试图将GrandChild*转换为Child*,事实上也转换成功了,同时指针的值并没有发生改变。GrandChild是普通继承于Child的,而非虚拟继承,换言之,Child不是GrandChild的虚基类,所以指针转换时,目标指针的值和赋给它的值保持一致。通过这样的分析我们似乎可以得出下面的结论:

    当一个派生类对象的指针转换到虚基类指针时(不管两者之间是否有其他中间类,而且也不管这些中间类是否是派生类的普通基类还是虚基类),指针的值就会发生变化

    Child和Grandchild的内存分布图:

    为了验证上述结论,在上面的基础上,我们将GrandChild改为虚拟继承Child,修改后的GrandChild代码如下:

    #include <iostream>
    using namespace std;
    class Parent
    {
    public:
        int parent;
    };
    class Child : public virtual Parent
    {
    public:
        int child;
    };
    class GrandChild : public virtual Child
    {
    public:
        int grandchild;
    };
    int main(void)
    {
        Child* pc = new Child();
        GrandChild* pgc = new GrandChild();
        cout << "1. The address of Child object:		";
        cout << (unsigned long*)pc << endl;
        cout << "2. The address of GrandChild object:	";
        cout << (unsigned long*)pgc << endl;
        // 将类Child对象的指针pc,向上转型(upcast)到其父类型指针,即Parent*
        Parent* pp = (Parent*)pc;
        cout << "3. Child* casted to Parent*:		";
        cout << (unsigned long*)pp << endl;
        // 将类GrandChild对象的指针pgc,向上转型(upcast)到其父类型指针,即Child*
        Child* pc2 = (Child*)pgc;
        cout << "4. GrandChild* casted to Child*:	";
        cout << (unsigned long*)pc2 << endl;
        // 再将上面通过转型得到的类Child对象的指针pc2,向上转型(upcast)到其父类型指针,即Parent*
        Parent* pp2 = (Parent*)pc2;
        cout << "5. Child* casted to Parent*:		";
        cout << (unsigned long*)pp2 << endl;
        // 将类GrandChild对象的指针pgc,向上转型(upcast)到其祖类型指针,即Parent*
        Parent* pp3 = (Parent*)pgc;
        cout << "6. GrandChild* casted to Parent*:	";
        cout << (unsigned long*)pp3 << endl;
        return 0;
    }

    运行程序,得到如下结果:

    与上面两个图相比较:

    我们看到,现在第4条也发生了变化。因此原来的结论是成立的。再次总结一下这条非常重要的结论:

    如果没有虚拟继承,当将派生类对象的指针转换到基类时(即使基类中有虚函数),指针的值不会发生变化;但当一个派生类对象的指针转换到虚基类指针时(不管两者之间是否有其他中间类,而且也不管这些中间类是否是派生类的普通基类还是虚基类),指针的值就会发生变化。

     Grandchild的内存分布图:

    这个结论对后面的了解含有虚基类的对象内存布局有着非同一般的意义。对于这个结论,我们还剩下一个问题,那就是为什么会这样呢? 前面我们可以看到赋值后的指针的值并不等于赋给它的对象地址值。也就是说在这个赋值过程中编译器进行了额外的工作,即调整了指针的值。我们看看上面程序中Parent* pp = (Parent*)pc; (向上类型转换,即up-casting) 这行对应的汇编代码(在VC中,进行debug时,按Alt 8,即可查看到汇编代码),看看编译器究竟做了些什么?

    38:       Parent* pp = (Parent*)pc;
    
    00401691   cmp         dword ptr [ebp-10h],0
    
    00401695   jne          main+120h (004016a0)
    
    00401697   mov         dword ptr [ebp-40h],0
    
    0040169E   jmp         main+12Eh (004016ae)
    
    004016A0   mov         eax,dword ptr [ebp-10h]
    
    004016A3   mov         ecx,dword ptr [eax]                           // 6
    
    004016A5   mov         edx,dword ptr [ebp-10h]                   // 7
    
    004016A8   add         edx,dword ptr [ecx+4]                       // 8
    
    004016AB   mov         dword ptr [ebp-40h],edx
    
    004016AE   mov         eax,dword ptr [ebp-40h]
    
    004016B1   mov         dword ptr [ebp-18h],eax

    重要的是第6、7、8行代码,它们通过偏移值指针找到偏移值,并以此来调整指针的位置,让目的指针最终指向对象中的基类部分的数据成员。

    至此,我们解释清楚了上面的问题。因为这部分讨论的结果太重要了,我们不妨再次总结如下:

    如果没有虚拟继承,当将派生类对象的指针转换到基类时(即使基类中有虚函数),指针的值不会发生变化;但当一个派生类对象的指针转换到虚基类指针时(不管两者之间是否有其他中间类,而且也不管这些中间类是否是派生类的普通基类还是虚基类),指针(目的指针)的值就会发生变化,目的指针最终指向对象中的基类部分(或曰基类的实例)

  • 相关阅读:
    简体中文和繁体中文的转换
    EasyPoi 快速Office 开发
    玩转SpringBoot之定时任务详解
    Spring Boot中使用Swagger2构建强大的RESTful API文档
    一篇文章带你搞懂 SpringBoot与Swagger整合
    JAVA团队开发手册
    CentOS(6、7)修改主机名(hostname)
    SpringBoot多环境部署,在启动时动态设置相应的配置文件
    MyBatis逆向工程代码的生成以及使用详解(持续更新)
    MySQL写入插入数据优化配置
  • 原文地址:https://www.cnblogs.com/wuchanming/p/4092049.html
Copyright © 2011-2022 走看看