在继承体系中,C++的标准并没有规定是基类成员放在前面还是派生类成员放在前面。但在具体的实现中,通常基类成员会放置在派生类成员的前面。但也有例外,这个例外就是当继承体系中具有虚基类时。

没有多态的继承(Inheritance without Polymorphism)

对于下面这种实继承,一般情况下不会增加空间与时间的开销。采用这种设计可以清晰的表达两个类的关系,但是这种设计的缺点是什么呢?

class Point2D
{
public:float x;float y;void operator+=(Point2D& p){this->x += p.x;this->y += p.y;}
};class Point3D
{
public:float x;void operator+=(Point3D& p){Point2D(p);this->z += p.z;}
};

这种设计的缺点之一是,可能增加函数的调用次数。比如对于Point3D::operator+=() ,其在内部会再次调用Point2D::operator+=(),增加了一次函数调用的开销。因此在继承体系中,合理的内联化函数也是非常重要的优化。第二个缺点在于,将一个类拆分成两个或多个继承层次的类可能会造成内存的膨胀。

以下面个类为例,其类对象的内存大小为8个字节。成员x占四个字节,成员a、b、c分别占一个字节,最后加上一个字节的内存对其,这时候类Concrete的总大小为8字节。但如果将这个类拆分成一个具有三层继承关系的类呢?

class Concrete
{
private:int x;char a;char b;char c;
};class Concrete1
{
protected:int x;char a;
};class Concrete2 : public Concrete1
{
protected:char b;
};class Concrete3 : public Concrete2
{
protected:char c;
};

此时,类Concrete3的大小变成了16字节(在我的编译器上,Concrete3的大小仍然是8字节,看来是编译器进行了优化)。对于类Concrete1来说,成员x占4个字节,成员a占1个字节,考虑字节对齐之后,类Concrete1的内存大小是8字节。而类Concrete2中的成员b,并不会被放置在Concrete1由于内存对齐而浪费的内存中,而是被放置在了类Concrete1中3字节对齐的后面。这样的话,考虑字节对齐,类Concrete2的内存大小就是12字节。基于同样的原因,Concrete3的内存大小是16字节。

为什么编译器要做这样看起不那么聪明的事情?编译器这样做是基于考虑了继承层次中的非同类型赋值所造成的内存覆盖问题。说起来有点不清晰,具体看下面的代码。

Concrete1 p_c1, p_c2;
Concrete1 c1;
Concrete2 c2;p_c1 = &c1;
p_c2 = &c2;*p_c1 = *p_c2;

如果在类Concrete2中,b放在a的下一个字节,那么通过*p_c1 = *p_c2 进行拷贝时,b的内容将会被拷贝到类Concrete1的内存空间中,这显然不合理。所以为了避免这种情况,编译器没有将继承体系中多个类的数据成员紧密排布。

但现在的编译器应该对这种情况进行了优化,在我本地的编译器(clang)上,类Concrete1、Concrete2、Concrete3具有相同的8字节大小。如果将类中的protected换成public,类Concrete1、Concrete2、Concrete3的大小分别为8、12、12字节。本来想基于这个现象继续探索下去,但是却发生了如下述代码所示的事情。可见,编译器做了些很难揣测的优化。将char换成int类型后,对象的内存空间就很清晰了。

class Concrete1
{
public:int x;char a;Concrete1() {x = 0; a = 'a';}
};Concrete1 c1;cout << "address of c1.x = " << &c1.x << endl;
cout << "address of c1.a = " << &(c1.a) << endl;// 程序输出
// address of c1.x = 0x7ffee395a980
// address of c1.a = a

具有多态的继承体系(Adding Polymorphism)

当在继承层次中出现多态时,我们都知道会发生什么。具有多态的类多有一个虚表指针,该虚表指针指向一个虚函数表,虚函数表中存储虚函数的地址,也有可能会存储跟类的类型相关的一些信息。那么当类具有多态时,其空间和时间上的开销主要有以下几个方面:

  • 每个类会有一个虚函数表,虚函数表存储虚函数的地址及类的RTTI(run time type info)

  • 每个类对象都有一个虚表指针,指向该类的虚函数表。

  • 在构造函数中初始化虚表指针。

  • 在析构函数中重置虚表指针。

后面还有一些讨论是与虚表指针在对象的内存空间中的位置的。一开始,C++的实现为了兼容C的struct,将虚表指针放在了对象内存空间的结尾。但是,为了支持多继承和虚基类,以及随着面向对象思维的发展,越来越多的C++实现开始将虚标指针放在对象内存空间的开头。

多继承(Multiple Inheritance)

在单继承的多态中,如果将虚表指针放在对象内存空间的开头时,有时候需要编译器的干预来实现指针(引用)的正确转换。比如下面这个类设计,基类Point并没有虚函数,当然也没有虚表指针,派生类Line具有虚表指针。如果虚表指针存放在派生类对象内存空间的开头,那么将一个派生类对象的地址赋值给一个基类指针时,该基类指针不应该指向派生类对象内存空间开头的部分,而应该偏移一个虚表指针的地址。这时候就需要编译器的干预,以使基类指针能够指向正确的地址。那么当多继承和虚继承出现时,就更加需要编译器的干预了。

class Point
{};class Line : public Point
{
public:virtual void Show(){}
};Point* p_ptr = nullptr;
Line l;p_ptr = &l;

多继承相对于单继承会更加的复杂,其复杂体现在派生类与基类的“不自然”的转换关系。如以下继承体系。如果将Vertex3d转换为Point2d,这种转换关系与人类的认知是相矛盾的,所以显得不自然。

class Point2d {
protected:float x, y;
};class Point3d : public Point2d {
protected:float z;
};class Vertex {
protected:Vertex* next;
};class Vertex3d : public Vertex, public Point3d {
protected:float mumble;
};

多继承的难点在于其对派生类与基类或者后续基类对象直接转换或者通过虚函数机制间接转换的影响。 这个继承体系的内存空间如下(这里以书中的为准,现实中vptr应该会被放在类内存空间的起始位置)。

( 为什么Vertex3d没有虚函数表呢?我目前的推测是Vertex3d中__vptr__Point2指向的虚函数表与Point2d中__vptr__Point2d指向的虚函数表不是同一个。Vertex3d中__vptr__Point2指向的虚函数表既包含了Point2d的虚函数地址(如果没有虚函数的重写的话)也包含了Point3d自身的虚函数地址。同样Vertex3d中__vptr__Point3d中也是如此。)

这上述的多继承中,如果将Vertex3d转换为Vertex的话,由于其首地址相同,所以不需要特别的操作。但是如果将Vertex3d转换为Point3d呢?此时就需要在Vertex3d地址的基础上加上Vertex对象的大小了。可见在多继承的情况下,编译器要在背后偷偷做很多工作。

虚继承(Virtual Inheritance)

如果多继承的情况下已经很麻烦了,那如果多继承中还有虚继承呢?其情况又会复杂一点。比如下面这个类。

class ios {};class istream : public virtual ios {};
class ostream : public virtual ios {};class iostream : public iostream, public ostream {};

在虚继承中,比较难的一点是,编译器应该在iostream的内存空间中如何放置ios才能在将iostream转化为istream或者ostream时(多态),仍然能够访问得到ios?通常的实现是将具有虚基类的对象的内存空间分成两个部分,一部分是不变区域,另一部分是共享区域。不变区域内的数据与对象内存空间的起始地址保持一个固定的位移,因此,不变区域内的数据成员可以直接访问。而共享区域则存放着虚基类的子对象(subobject)。共享区域内数据的位置会随着每次派生而改变。所以,位于共享区的数据成员的访问需要间接来进行。而各种不同C++实现的区别也就在于共享区内数据成员的间接访问方式。

通过下面这个虚继承的类,我们可以看看虚继承的几种实现方式,及其优缺点。

class Point2d
{
protected:float x, y;
};class Vertex : public virtual Point2d
{
protected:Vertex* next;
};class Point3d : public virtual Point2d
{
protected:float z;
};class Vertex3d : public virtual Vertex, public virtual Point3d
{
protected:float mumble;
};

在cfront最初的实现里,会在派生类中插入一个指向基类的指针,访问基类数据成员的时候通过这个指针来间接的访问。比如下面这个函数,对数据成员x和y的访问,会被转化为指针的形式。这样的转换,同样发生在指针转换的时候。

void Point3d::operator+=(const Point3d& rhs)
{x += x;y += y;z += z;
}//伪代码
__vbcPoint2d->x += rhs.__vbcPoint2d->x;
__vbcPoint2d->y += rhs.__vbcPoint2d->y;
z += rhs.z;Vertex* v_p = p3_p;//伪代码
Vertex* v_p = p3_p ? p3_d->__vbcPoint2d : 0;

但是,上面这种实现有两个明显的缺点。第一,派生类有几个虚基类,就有几个指针,比如Vertex3d中就有两个指向Point2d的指针,这样会浪费内存。第二,如果整个派生层次很多,那么会发生多次间接的寻址,运行效率不够高。

针对第一个问题,MicroSoft的解决办法是生成一个虚基类指针表,然后派生类中只有一个指向该虚拟基类表的指针。这样会减少虚基类指针的个数,但增加了一次寻址。还有一种解决办法是,在派生类中的虚函数表中存储每个虚基类成员在派生类内存空间中相对于派生类对象地址的偏移量,这样就不需要存储多个指针。

针对第二个问题,作者没有进一步讨论,估计是没办法。

总之,使用虚拟基类最好的场景是,虚拟基类是一个纯虚类,不含有任何数据成员。

Inside C++ Object Model: Inheritance and the Data Member相关推荐

  1. 《深度探索C++对象模型(Inside The C++ Object Model )》学习笔记

    来源:http://dsqiu.iteye.com/blog/1669614 之前一直对C++内部的原理的完全空白,然后找到<Inside The C++ Object Model>这本书 ...

  2. Data Member 的存取

    考察以下代码: Point3d origin; origin.x = 0.0; 此例中 x 的存取成本是什么? 答案则是视 x 和 Point3d 而定(别打脸, 我知道这是废话). 具体的呢? 因为 ...

  3. Inside the C++ Object Model | Outline

    <Inside the C++ Object Model(C++对象模型)>,这是一本灰常不错的书! CSDN下载页面(中文,侯捷译) 豆瓣评论 读书笔记目录如下(不定时更新): 转载于: ...

  4. inside the C++ Object model总结

    一. 关于对象 1.内联函数:能够除去函数调用的开支,每一处内联函数的调用都是代码的复制.这是一种空间换取时间的做法,若函数代码量大或者有循环的情况下,不宜内联(这件事有些编译器会自动帮你做).在类中 ...

  5. Inside C++ object Model--对象模型概述

    在C中, "数据"和"处理数据的操作"是分开声明的, 语言本身并没有支持"数据和函数"之间的关联性. 这种称为"procedura ...

  6. Sharepoint学习笔记 –架构系列—10 Sharepoint的服务器端对象模型(Server Object Model) 2.内容层次结构

    Sharepoint的内容层次结构(Content Hierarchy)包括表示可发布数据项(publishable items),如列表项的类,还包括表示嵌套的数据容器(nested contain ...

  7. Component Object Model (COM)

    [简介] COM是什么?COM怎么来的?为什么要有COM?COM是怎么工作的?COM组件,COM对象,COM接口关系? COM (Component Object Model, 组件对象模型) 是一种 ...

  8. MOSS 2010:Visual Studio 2010开发体验(19)——ECMAScript Object Model

    这篇文章部分材料摘自下面这个地址,我做了翻译,并且按照我的案例场景做了补充 http://www.codeproject.com/Articles/60348/SharePoint-2010-Clie ...

  9. 【论文阅读】Deep Compositional Captioning: Describing Novel Object Categories without Paired Training Data

    [论文阅读]Deep Compositional Captioning: Describing Novel Object Categories without Paired Training Data ...

最新文章

  1. JAVA-Eclipse快捷键
  2. upload-labs_pass20-move_uploaded_file函数特性
  3. GNU ARM 汇编指令[转载]
  4. 配置中文_星球大战:战机中队配置需求公布 支持中文
  5. SpringCloud系列-Ribbon的基本应用
  6. Jmeter简单的登录压力测试(使用json发送post请求)
  7. 顶配12599元!三星Galaxy S22国行价格来了...
  8. 易语言怎么判断文件是否一样_怎么判断自己是否怀孕?
  9. java中random方法取值范围_java中最值的求法,你可能忽略了这种方法了!
  10. VS2010 VB.net安装包生成过程
  11. 惠普打印机驱动服务器系统安装教程,最简单的安装惠普1020打印机驱动的方法...
  12. 计算机用户名显示TEMP,win10只要打开ie桌面出现temp文件夹如何解决
  13. Kotlin by lazy解析及在findviewById场景中的使用
  14. 【持续更新】uni-app学习笔记
  15. php将一维数组转换成二维数组
  16. matlab第四章图像复原与重建
  17. GBase 8d证书查看
  18. 信息管理导论 | 信息组织
  19. ccie和hcie的内容差距大吗?
  20. 创客(米思奇编程)-04-点阵屏的控制

热门文章

  1. IoTLink版本跟新V1.10.0
  2. TexStudio的安装与使用教程(包括参考文献的引用)Latex教程
  3. Texstudio下载
  4. MintUI 组件和MUI组件
  5. R型变压器220v和110v电压有什么不同的用途?
  6. MySQL 到 MySQL 实时数据同步实操分享
  7. 区分实时数据、离线数据、流式数据以及批量数据的区别
  8. keil在线下载pack包
  9. 【计算机网络】扩展以太网方法总结
  10. 百度地图坐标转换为gps_百度地图与中交兴路合作,为大卡司机提供专业导航服务...