对象内存布局 (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:\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行代码,它们通过偏移值指针找到偏移值,并以此来调整指针的位置,让目的指针最终指向对象中的基类部分的数据成员。

 

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

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

时间: 2024-10-25 01:31:32

对象内存布局 (11)的相关文章

对象内存布局 (7)

在对象内存布局 (5)的代码中,在Derived类中覆盖Base1中声明的vfBase1_1(),其他代码不变.修改后的代码如下: #include <iostream> using namespace std; class Base1 { public: int m_base1; inline virtual void vfBase1_1() { cout << "This is in Base1::vfBase1_1()" << endl; }

对象内存布局 (6)

如果在对象内存布局 (5)的代码中,将Base1中的两个虚函数声明删除,同时将main函数中的下面代码注释掉(因为现在只有两张虚函数表了): 代码如下: #include <iostream> using namespace std; class Base1 { public: int m_base1; /*inline virtual void vfBase1_1() { cout << "This is in Base1::vfBase1_1()" <

对象内存布局 (12)

下面来看看虚基类对对象内存布局的影响.虚基类的主要作用就是在所有的派生类中,保留且仅保留一份虚基类的suboject.   a. 一个虚基类的情况 #include <iostream> using namespace std; class Base { public: int base_member; }; class Derived : public virtual Base {}; int main(void) { Base b; Derived d; cout << siz

对象内存布局 (10)

在对象内存布局 (9)基础上做些修改:派生类override基类的虚函数,即Base2 override Base1中声明的虚函数vfBase1(),Base3 override Base1中声明的虚函数vfBase1()和Base2中声明的虚函数vfBase2(), Derived override Base1中声明的虚函数vfBase1().Base2中声明的虚函数vfBase2()和Base3中声明的虚函数vfBase3().修改如下: #include <iostream> using

对象内存布局 (14)

前言 07年12月,我写了一篇<C++虚函数表解析>的文章,引起了大家的兴趣.有很多朋友对我的文章留了言,有鼓励我的,有批评我的,还有很多问问题的.我在这里一并对大家的留言表示感谢.这也是我为什么再写一篇续言的原因.因为,在上一篇文章中,我用了的示例都是非常简单的,主要是为了说明一些机理上的问题,也是为了图一些表达上方便和简单.不想,这篇文章成为了打开C++对象模型内存布局的一个引子,引发了大家对C++对象的更深层次的讨论.当然,我之前的文章还有很多方面没有涉及,从我个人感觉下来,在谈论虚函数

对象内存布局 (13)——上一篇的纠正

下面来看看虚基类对对象内存布局的影响.虚基类的主要作用就是在所有的派生类中,保留且仅保留一份虚基类的suboject. #include <iostream> using namespace std; class Base { public: int m_base; Base():m_base(20){} virtual void vfBase_1() { cout << "This is in Base::vfBase_1()" << endl;

对象内存布局 (15)

重复继承   下面我们再来看看,发生重复继承的情况.所谓重复继承,也就是某个基类被间接地重复继承了多次.   下图是一个继承图,我们重载了父类的f()函数.   其类继承的源代码如下所示.其中,每个类都有两个变量,一个是整形(4字节),一个是字符(1字节),而且还有自己的虚函数,自己overwrite父类的虚函数.如子类D中,f()覆盖了超类的函数, f1()和f2() 覆盖了其父类的虚函数,Df()为自己的虚函数. class B {     public:         int ib;  

vs2008下C++对象内存布局(3):加上虚函数

这次我们为父类加上虚函数: class CParent { public: int parent_a; int parent_b; public: virtual void parent_f1() { parent_a = 0x10; } virtual void parent_f2() { parent_b = 0x20; } }; class CChild : public CParent { public: int child_a; int child_b; public: virtual

对象内存布局 (1)

内容概要: 满足下面2个条件时, 1. 父类有虚函数,子类无虚函数(即无虚函数重写或无虚函数覆盖) 2. 非虚继承 类对象之内存布局   前述相关内容参考: 1. http://blog.csdn.net/pathuang68/archive/2009/04/20/4096088.aspx 2. http://blog.csdn.net/pathuang68/archive/2009/04/21/4096429.aspx 3. http://blog.csdn.net/pathuang68/ar