【深度探索C++对象模型】第三章 Data语意学

C++对象模型尽量以空间优化和存取速度优化的考虑来表现nonstatic data members,并且保持和C语言struct数据配置的兼容性,它把数据直接存放在每一个class object中。对于继承而来的nonstatic data members也是如此。不过并没有强制定义其间的排列顺序。至于static data members,则被放置在程序的一个global data segment中,不会影响个别class object的大小,static data members永远只存在一份实例。

​ 每一个class object必须有足够的大小以容纳它所有的nonstatic data members。但有时候可能比预想的还大,原因是:

  1. 有编译器自动加上额外的data members,用以支持某些语言特性(主要是各种virtual特性);

  2. 因为alignment(边界调整)的需要。

Data member的绑定

Data member的布局

已知下面一组data member:

  
  class Point3d{
  public:
      //……
  private:
      float x;
      static List<Point3d*> freeList;
      float y;
      static const int chuckSize=250;
      float z;
  };

Nonstatic data member在class object中的排列顺序和其被声明的顺序一样,任何中间介入的static data member如freeList、chuckSize都不会被放进对象布局中。在上例中Point3d对象是由3个float组成的,顺序是x,y,z。static data members存放在程序的data segment中,和个别的class object无关。

​ C++标准要求,在同一个access section(也就是private、public、protected等区段)中,members的排列顺序只需符合“较晚出现的members在class object中较高的地址”这一条即可。也就是说,各个members并不一定得连续排列。members的边界调整(alignment)可能需要补充一些bytes。

​ 编译器还会合成一些内部使用的data members,以支持整个对象模型。vptr就是这样的东西。vptr传统上它被安放在所有显示声明memebers的最后,不过如今也有一些编译器把vptr放在一个class object的最前端。

​ C++允许编译器将多个access sections中的data members自由排列,不必在乎它们出现在class声明中的顺序,不过目前没有编译器这么做。

Data member的存取

已知下面这段代码:

  
  Point3d origin;
  origin.x=0.0;

​ x的存取成本是什么?答案视x和Point3d如何声明而定。x可能是个static member,也可能是nonstatic member。Point可能是个独立(非派生)的class,也可能是从另一个单一的base class派生而来。

​ 抛出问题。如果有两个定义,origin和pt:

  
  Point3d origin,*pt=&origin;

​ 用它们存取data members,例如:

  
  origin.x=0.0;
  pt->x=0.0;

​ 两种存取方式是否有重大差异?

Static Data Members

​ 类的每个static成员都只有一个实例,存放在程序的data segment之中,和对象无关;因此对于这种情况,对x的存取并不会招致任何空间和时间上的额外负担。从指令执行的观点来看,这是C++语言中”通过一个指针和通过一个对象来存取member,结论完全相同“的唯一一种情况。

Nonstatic Data Members

​ 欲对一个nonstatic data members进行存取操作,编译器需要把class object的起始地址加上data member的偏移地址。例如:

  
  //源代码
  origin._y=0.0;
  //编译后的代码
  *(&origin+(&Point3d::_y-1))=0.0;

"继承"与Data member

只要继承不要多态

​ 在有继承的情况下,可能会导致空间上的浪费。我们来看这样一个例子:

  
  class Concrete{
  public:
      //……
  private:
      int val;
      char c1;
      char c2;
      char c3;
  };

​ 这个类中存有一个int和三个char,如果我们把这些变量都放到一个类中声明,那么算上alignment,它的对象大小为8字节。

​ 现假设Concrete分裂为三层结构:

  
  class Concrete1{
  public:
      //……
  private:
      int val;
      char bit1;
  };
  
  class Concrete2:public Concrete1{
  public:
      //……
  private:
      char bit2;
  };
  
  class Concrete3:public Concrete2{
  public:
      //……
  private:
      char bit3;
  };

​ 那么Concrete3的对象的大小将达到16字节,比原先的设计多了100%!

​ 这是因为alignment(边界调整)导致的,因为C++的对象模型中,在一个继承而来的类的内存分布里,各个基类需要分别遵循alignment,从而导致了空间的浪费。具体地对象布局可见下图:

加上多态

​ 在这种情况下,无论是时间还是空间上,访问类的成员都会带来一定额外的负担,主要体现在以下几个方面:

  1. virtual table,用来存放它所声明的每一个virtual functions的地址。

  2. 每一个对象中会有一个vptr,提供执行期的链接。

  3. 编译器会重写constructor和destructor,使其能够创建和删除vptr

多重继承

​ 多重继承既不像单一继承,也不容易模塑出其模型。多重继承的复杂度在于derived class和其上一个base class乃至上上一个base class……之间的“非自然”关系。例如,考虑下面这个多重继承所获得的class vertex3d:

  
  class Point2d{
  public:
      //……
  protected:
     float _x,_y;
  };
  
  class Point3d:public Point2d{
  public:
      //……
  protected:
     float _z;
  };
  
  class Vertex{
  public:
      //……
  protected:
      Vertex *next;
  };
  
  class Vertex3d:public Point3d,public Vertex{
  public:
      //……
  protected:
      float mumble;
  };

继承关系如下:

​ 对一个多重派生对象,将其地址指定给”最左端(也就是第一个)base class的指针“,情况将和单一继承时相同,因为二者都指向相同的起始地址。需付出的成本只有地址的指定操作而已。至于第二个或后继的base class的地址指定操作,则需要将地址修改过:加上介于中间的base class subobject大小,例如:

  
  Vertex3d v3d;
  Vertex *pv;
  Point2d *p2d;
  Point3d *p3d;

对于操作:

  
  pv=&v3d;

需要这样的内部转化:

  
  //虚拟C++代码
  pv=(Vertex*)(((char*)&v3d)+sizeof(Point3d));

而对于下面操作:

  
  p2d=&v3d;
  p3d-&v3d;

只需要简单地拷贝其地址就好。

虚拟继承

​ 在虚拟继承中,C++对象模型将Class分为两个区域,一个是不变区域,直接存储在对象中;一个是共享区域,存储的是virtual base class subobjects,它在内存中单独存储在某处,derived class object持有指向它的指针。在cfront编译器中,每一个derived class object中安插一些指针,每个指针指向一个virtual base class,为此需要付出相应的时间和空间成本。如下所示:

  
  class Point2d{
  public:
      //……
  protected:
     float _x,_y;
  };
  
  class Point3d:public virtual Point2d{
  public:
      //……
  protected:
     float _z;
  };
  
  class Vertex:public virtual Point2d{
  public:
      //……
  protected:
      Vertex *next;
  };
  
  class Vertex3d:public Point3d,public Vertex{
  public:
      //……
  protected:
      float mumble;
  };
  //具体的类同上一节多重继承,不同的是Vertex和Point3d虚拟继承了Point2d
  void Point3d::operator+=(const Point3d&rhs)
  {
      x+=rhs.x;
      y+=rhs.y;
      z+=rhs.z;
  }
   
  //编译器翻译后的版本
  _vbcPoint2d->x+=rhs._vbcPoint2d->x;
  _vbcPoint2d->y+=rhs._vbcPoint2d->y;
  z+=rhs.z;

猜你喜欢

转载自blog.csdn.net/u012940886/article/details/80375849