对象内存布局 (11)

简介: 注意:关于内存对齐(memory alignment),请看关于内存对齐问题,后面将会用到。   下面我们进行在普通继承(即非虚继承)时,派生类的指针转换到基类指针的情形研究。假定各类之间的关系如下图:   代码如下: #include using namespace std;...

注意:关于内存对齐(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:\t\t";
    cout << (unsigned long*)pc << endl;
    cout << "2. The address of GrandChild object:\t";
    cout << (unsigned long*)pgc << endl;
    // 将类Child对象的指针pc,向上转型(upcast)到其父类型指针,即Parent*
    Parent* pp = (Parent*)pc;
    cout << "3. Child* casted to Parent*:\t\t";
    cout << (unsigned long*)pp << endl;
    // 将类GrandChild对象的指针pgc,向上转型(upcast)到其父类型指针,即Child*
    Child* pc2 = (Child*)pgc;
    cout << "4. GrandChild* casted to Child*:\t";
    cout << (unsigned long*)pc2 << endl;
    // 再将上面通过转型得到的类Child对象的指针pc2,向上转型(upcast)到其父类型指针,即Parent*
    Parent* pp2 = (Parent*)pc2;
    cout << "5. Child* casted to Parent*:\t\t";
    cout << (unsigned long*)pp2 << endl;
    // 将类GrandChild对象的指针pgc,向上转型(upcast)到其祖类型指针,即Parent*
    Parent* pp3 = (Parent*)pgc;
    cout << "6. GrandChild* casted to Parent*:\t";
    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:\t\t";
    cout << (unsigned long*)pc << endl;
    cout << "2. The address of GrandChild object:\t";
    cout << (unsigned long*)pgc << endl;
    // 将类Child对象的指针pc,向上转型(upcast)到其父类型指针,即Parent*
    Parent* pp = (Parent*)pc;
    cout << "3. Child* casted to Parent*:\t\t";
    cout << (unsigned long*)pp << endl;
    // 将类GrandChild对象的指针pgc,向上转型(upcast)到其父类型指针,即Child*
    Child* pc2 = (Child*)pgc;
    cout << "4. GrandChild* casted to Child*:\t";
    cout << (unsigned long*)pc2 << endl;
    // 再将上面通过转型得到的类Child对象的指针pc2,向上转型(upcast)到其父类型指针,即Parent*
    Parent* pp2 = (Parent*)pc2;
    cout << "5. Child* casted to Parent*:\t\t";
    cout << (unsigned long*)pp2 << endl;
    // 将类GrandChild对象的指针pgc,向上转型(upcast)到其祖类型指针,即Parent*
    Parent* pp3 = (Parent*)pgc;
    cout << "6. GrandChild* casted to Parent*:\t";
    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:\t\t";
    cout << (unsigned long*)pc << endl;
    cout << "2. The address of GrandChild object:\t";
    cout << (unsigned long*)pgc << endl;
    // 将类Child对象的指针pc,向上转型(upcast)到其父类型指针,即Parent*
    Parent* pp = (Parent*)pc;
    cout << "3. Child* casted to Parent*:\t\t";
    cout << (unsigned long*)pp << endl;
    // 将类GrandChild对象的指针pgc,向上转型(upcast)到其父类型指针,即Child*
    Child* pc2 = (Child*)pgc;
    cout << "4. GrandChild* casted to Child*:\t";
    cout << (unsigned long*)pc2 << endl;
    // 再将上面通过转型得到的类Child对象的指针pc2,向上转型(upcast)到其父类型指针,即Parent*
    Parent* pp2 = (Parent*)pc2;
    cout << "5. Child* casted to Parent*:\t\t";
    cout << (unsigned long*)pp2 << endl;
    // 将类GrandChild对象的指针pgc,向上转型(upcast)到其祖类型指针,即Parent*
    Parent* pp3 = (Parent*)pgc;
    cout << "6. GrandChild* casted to Parent*:\t";
    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行代码,它们通过偏移值指针找到偏移值,并以此来调整指针的位置,让目的指针最终指向对象中的基类部分的数据成员。

 

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

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

相关文章
|
26天前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
51 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
|
26天前
|
Java 测试技术 Android开发
让星星⭐月亮告诉你,强软弱虚引用类型对象在内存足够和内存不足的情况下,面对System.gc()时,被回收情况如何?
本文介绍了Java中四种引用类型(强引用、软引用、弱引用、虚引用)的特点及行为,并通过示例代码展示了在内存充足和不足情况下这些引用类型的不同表现。文中提供了详细的测试方法和步骤,帮助理解不同引用类型在垃圾回收机制中的作用。测试环境为Eclipse + JDK1.8,需配置JVM运行参数以限制内存使用。
30 2
|
26天前
|
存储 Java
JVM知识体系学习四:排序规范(happens-before原则)、对象创建过程、对象的内存中存储布局、对象的大小、对象头内容、对象如何定位、对象如何分配
这篇文章详细地介绍了Java对象的创建过程、内存布局、对象头的MarkWord、对象的定位方式以及对象的分配策略,并深入探讨了happens-before原则以确保多线程环境下的正确同步。
48 0
JVM知识体系学习四:排序规范(happens-before原则)、对象创建过程、对象的内存中存储布局、对象的大小、对象头内容、对象如何定位、对象如何分配
|
1月前
|
存储 Java
深入理解java对象的内存布局
这篇文章深入探讨了Java对象在HotSpot虚拟机中的内存布局,包括对象头、实例数据和对齐填充三个部分,以及对象头中包含的运行时数据和类型指针等详细信息。
28 0
深入理解java对象的内存布局
|
1月前
|
存储 编译器 C++
【C++】掌握C++类的六个默认成员函数:实现高效内存管理与对象操作(二)
【C++】掌握C++类的六个默认成员函数:实现高效内存管理与对象操作
|
28天前
|
算法 Java
JVM进阶调优系列(3)堆内存的对象什么时候被回收?
堆对象的生命周期是咋样的?什么时候被回收,回收前又如何流转?具体又是被如何回收?今天重点讲对象GC,看完这篇就全都明白了。
|
1月前
|
存储 编译器 C++
【C++】掌握C++类的六个默认成员函数:实现高效内存管理与对象操作(三)
【C++】掌握C++类的六个默认成员函数:实现高效内存管理与对象操作
|
1月前
|
存储 编译器 C++
【C++】掌握C++类的六个默认成员函数:实现高效内存管理与对象操作(一)
【C++】掌握C++类的六个默认成员函数:实现高效内存管理与对象操作
|
3月前
|
存储 程序员 Python
Python类的定义_类和对象的关系_对象的内存模型
通过类的定义来创建对象,我们可以应用面向对象编程(OOP)的原则,例如封装、继承和多态,这些原则帮助程序员构建可复用的代码和模块化的系统。Python语言支持这样的OOP特性,使其成为强大而灵活的编程语言,适用于各种软件开发项目。
32 1
|
2月前
crash —— 获取物理内存布局信息
crash —— 获取物理内存布局信息