c++派生类对象的内存布局

派生类对象的内存布局需满足的要求是,一个基类指针,无论其指向基类对象,还是派生类对象,通过它来访问一个基类中定义的数据成员,都可以用相同的步骤。

理解了这一要求,就能理解下面介绍的派生类内存布局的合理性了。对象的内存布局问题,并不是C+标准中明确规定的,不同的编译器可以有不同的实现。我介绍一种很多编译器在使用的、最为自然的和容易理解的对象内存布局方式。

c++派生类对象的内存布局

1.单继承的情况

单继承的情况比较简单。考虑下面的情况:

class Base{......};

class Derived: public Base{......};

那么在 Derived类的对象中, Derived从Base继承来的数据成员,全部放在前面,与这些数据成员在Base类的对象中放置的顺序保持一致, Derived类新增的数据成员全部放在后面,如图所示。如果出现了从 Derived指针到Base指针的隐含转换,例如:

Base*Ba=new Base(); //基类指针指向派生类对象

Derived*De=new Derived(); //派生类指针指向派生类对象

Base*Ba2=De; // 派生类指针转换基类指针

c++派生类对象的内存布局

单继承情况下的对象内存布局

在De赋给Ba2的过程中,指针值不需要改变。Ba和Ba2这两个Base类型的指针虽然指向的对象具有不同的类型,但任何一个Base数据成员到该对象首地址都具有相同的偏移量,因此使用Base指针Ba和Ba2访问Base类中定义的数据成员时,可以采用相同的方式,而无须考虑具体的对象类型。

2.多继承的情况

多继承的情况要比单继承稍微复杂一些,考虑下面这种情况:

class Base1{......};

class Base2{......};

class Derived:public Base1,public Base2{...};

Derived类继承了 Basel类和Base2类,在 Derived类的对象中,前面依次存放的是从Base类和Base2类继承而来的数据成员,其顺序与它们在 Basel类和Basc2类的对象中放置的顺序保持一致, Derived类新增的数据放在它们的后面,如图所示。Base如果出现了从 Derived指针到 Basel指针或Base2指针的隐含转换,例如:

Base1*Ba1a=new Base1();

Base2*Ba2a=new Base2();

Derived De=new Derived();

Base*Ba1b=De;

Base*Ba2b=De

c++派生类对象的内存布局

多继承情况下的对象内存布局

将pd赋给pblb指针时,与单继承时的情形相似,只需要把地址复制一遍即可。但将pd 赋给pb2b指针时,则不能简单地执行地址复制操作,而应当在原地址的基础上加一个偏 移量,使pb2b指针指向 Derived对象中Base2类的成员的首地址。这样,对于同为Base2类型指针的pb2a和pb2b来说,它们都指向Base2中定义的、以相同方式分布的数据成员。

虚拟继承的情况

虚拟继承的情况更加复杂,考虑下面的继承关系

class Base0{...};

class Base1:virtual public Base0{...};

class Base2:virtual public Base0{...};

class Derived:public Base1,public Base2{...};

Basel类型指针和Base2类型指针都可以指向Derived对象,而且通过这两类指针都可以访问Base0类中定义的数据成员,但这些数据成员在Derived对象中只有一份。因此,只能通过间接的方式来确定 Basel对象、Base2对象和 Derived对象中Base0数据成员的位置。具体的解决办法因编译器而异,一种比较容易理解的布局方式是,在 Basel类型对象和Base2类型对象中都增加一个隐含的指针,这个指针指向Base0中定义的数据成员的首地址。 Derived类同时继承了这两枚指针,但由于 Derived类中的Bac0类数据成员只有一份,因此 Derived类型对象中的这两个隐含指针指向相同的地址。因为继承了指向Base0类数据成员的指针,Base0数据成员放置的位置已经不重要了,一般来说可以放在最后。如图所示:

c++派生类对象的内存布局

虚拟继承下的对象内存布局(只是一种实现方式)

通过上面的讨论,我们还得到了一个意外收获:指针转换时并非都保持原先的地址不变,地址的算术运算有可能在指针转换时发生。能看到文章结尾的都是好孩子,感谢大家,喜欢我的文章请关注。


分享到:


相關文章: