《深度探索C++对象模型》读书笔记——Data语意学【for_wind】

//整理之,分享之,欢迎指正。for_wind

1、类的实际大小。

      每一个class object必须有足够的大小以容纳它所有的nonstatic data members。除了其nonstatic data members的总和大小外,其值的大小受以下三个因素影响:
(1) 语言本身所造成的额外负担:即为了支持virtual而由内部产生的额外负担。当语言支持virtual base class 时,在derived class 中,这个额外的负担体现在某种形式的指针 bptr身上,它或者指向virtual base class subobject,或者指向一个相关表格(表内存放virtual base class subobject的地址,即其偏移量offset);此外还有因虚函数产生的 vptr
(2) 编译器对特殊情况所提供的优化处理:某些编译器会对empty virtual base class提供特殊支持。
(3) alignment的限制:将数值填补到某数(如4bytes(32位))的整数倍,以使bus的“运输量”达到最高效率。

备注:
  • 一个空的class,其值的大小并不为零。它有一个隐晦的1byte,那是被编译器安插进去的一个char。这使得这个class的两个objects得以在内存中配置独一无二的地址。
  • 一个virtual base class subobject只会在derived class中存在一份实体,不管它在继承体系中出现了多少次。
举例:
下图给出class X, Y, Z, A之间的继承关系。 问class X, Y, Z, A的大小分别为多少。
    
下图给出X,Y,Z的对象布局(编译器不做优化处理)(1,8,8)

下图给出X,Y,Z的对象布局(编译器做了优化处理,不安插一个char)(1,4,4)

上例中的class A的大小又该如何计算,其内存布局又该如何?
(1)被Y、Z共享的 唯一的一个class X实体,大小为1byte;(对empty virtual base class特殊处理的编译器会将(1)中class X实体的1byte 拿掉)
(2)Base class Y的大小,减去“因virtual base class X而配置的大小”,结果是4bytes。同理Base calss Z。共8bytes。
(3)class A自己的大小为:0byte。
(4)class A的alignment数量。
      class A的大小,在有特殊优化的编译器上运行结果为8bytes;在无特殊优化的编译器上运行结果则为12bytes。
      注意:非空virtual base class两种编译器产生的对象布局一致。
下面给出VS编译器下的情况。
#include <iostream>

using namespace  std ;

class X{ };
class Y : public virtual X { };
class Z : public virtual X { };
class A : public Y, public Z { };

int main( void )
{
        cout <<"sizeof X=" << sizeof( X )<<endl ;
        cout <<"sizeof Y=" << sizeof( Y )<<endl ;
        cout <<"sizeof Z=" << sizeof( Z )<<endl ;
        cout <<"sizeof A=" << sizeof( A )<<endl ;
        return 0 ;
}
运行结果:

VS2010中 class X的内存布局:

VS2010中 class Y的内存布局:

VS2010中 class Z的内存布局:

VS2010中 classA 的内存布局:

分析:
      VS2010编译器中,对每个继承自虚基类的类实例,将增加一个隐藏的 “虚基类表指针”(vbptr)成员变量,从而达到间接计算虚基类位置的目的。该变量指向一个全类共享的偏移量表,表中项目记录了对于该类而言,“虚基类表指针”与虚基类之间的偏移量。
      class X的大小为1byte,而class Y,Z的大小为4bytes,含有一个vbptr,即该编译器把vbptr放在前面,对empty  virtual base class进行特殊处理,去除了1byte。
      而class A的大小的大小为8bytes,具体分析见上。

2、Data member的绑定

      对member functions函数本身的分析将延迟直至class声明的右大括号出现才开始,因此在一个inline member function 内的一个data member绑定操作,在整个class 声明完成之后才发生。
      然而,这对member function中的argument list并 不为真。Arugment list中的名称会在第一次遇到时被适当地决议resolved。因此最好把“nested type 声明”放在class的起始处,以确保非直觉绑定的正确性。
举例
#include <iostream>
using namespace  std;

typedef int length;

class Point3d
{
public:
     //typedef float length;//pos2
     void fun1(length val)
     {
          _val = val;
     }
     length fun1()
     {
          return _val;
     }
private:
     typedef float length;//pos1
     length _val;
};

int main(void)
{
     Point3d p;
     p.fun1(1.2);
     cout<<p.fun1()<<endl;
     return 0;
}
"nested type 声明”放在位置pos1:第一次遇到函数参数后,决议为global typedef,因此为int
运行结果:

"nested type 声明”放在位置pos2:注释掉pos1那句后
运行结果:

3、Data member的布局

(1)对于 nonstatic data member,把数据直接存放在每一个class object之中,继承而来的nonstatic data member也是如此,不过不强制其排列顺序。
(2)对于 static data member,则把被放在程序的global data segment中,永远只存在一份实体,和个别的class object无关。 template class稍微不同。
(3)access section(private、public、protected等区段)中members的排列 只需符合“较晚出现的members在class object中有较高的地址”。并非一定得连续。(凡处于同一个access section中的数据,必定保证以其声明的次序出现在内存布局当中,然而被放在多个access section的各个数据,排列次序就不一定了)但是当前的编译器都是把一个以上的access sections 连锁在一起,依照声明次序,形成一个连续区块。
(4)对于那些由编译器 内部产生的data members(如vptr)允许自由放在任何位置。当前所有编译器把vptr安插在每一个“内含virtual function的class”的object内(最后,或者最前)。

4、Data member的存取

(1)对于 static data member
      每次程序取用static member,就会被内部转换为对该唯一的extern实体的直接参考操作。存取static members并不需要通过class object。对于继承而来的static member其存取路径也是同样直接。(因为static members只存在唯一的一份实体)
(2)对于 nonstatic data member
      每一个nonstatic data member的偏移量offset,在编译时期即可获知。(派生自单一或多重继承串链也一样)。
而当虚继承时,虚继承将为“经由base class subobject 存取 class members”导入一层新的间接性。
总结:
      将对象分割为两部分,一个不变局部和一个共享局部。不变局部中的数据,不管后继如何衍化,总是拥有固定的偏移量,所以这一部分数据可以被直接存取,至于共享局部,所表现的就是虚拟继承的基类子对象,这一部分的数据,其位置会因为每次的派生操作而有变化,所以它们是间接存取。
举例:
Pointed origin, *pt = &origin;
origin.x = 0.0;
pt->x = 0.0;
从origin和pt存取有何差异?
      答:当Point3d是一个derived class,而在其继承结构中有一个virtual base class,并且被存取的member是一个从该virtual base class继承而来的member时,有差异。
      从pt存取,这时我们不知道pt指向哪一种class type,即无法知道编译时期这个member真正的offset位置,这个存取操作必须延迟至执行期,经由一个额外的间接引导,才能够解决。存取速度比较慢一些。从origin存取,origin的类型是明确的,members的offset位置在编译时期就固定了。

5、Data member与继承

(1)多态与多重继承

多态的弹性带来空间和存取时间上的额外负担:
  • 添加virtual table;
  • 添加vptr,提供执行期的链接。
  • 加强constructor,为vptr设定初值,让它指向class所对于的virtual table;
  • 加强destructor,消除指向class virtual table的vptr。
多重继承的问题:
在于derived class objects和其第二或其后继的base class objects之间的转换。
修正:加上或减去介于中间的base class subobjects大小。
(最左端或说第一个base class都指向相同的起始地址,无须修正)

举例1:

class Point2d
{
public:
     virtual void fun1(){ };
protected:
     float _x, _y;
};

class Point3d : public Point2d
{
public:
     virtual void fun1(){ };
protected:
     float _z;
};


class Vertex
{
public:
     virtual void fun2(){ };
protected:
     Vertex *next;
};

class Vertex3d : public Point3d, public Vertex
{
public:
     virtual void fun1(){ };
     virtual void fun2(){ };
     void fun3(){ };
protected:
     float mumble;
};
下图是VS2010中 class Vertex3d的内存布局(多重继承):

对比上下两图:
      除了vfptr的位置(在前或在后)不同,其他内容是一致的。对于单一继承和多重继承,VS2010编译器采用直接复制模型,将base class subobject的data members被直接放在derived class object中。
此外也注意到 -16。
下图是可能的数据分布(多重继承):


(2)虚继承


举例2:


class Point2d
{
public:
     virtual void fun1(){ };
protected:
     float _x, _y;
};

class Point3d : public virtual Point2d
{
public:
     virtual void fun1(){ };
protected:
     float _z;
};


class Vertex : public virtual Point2d
{
public:
     virtual void fun2(){ };
protected:
     Vertex *next;
};

class Vertex3d : public Point3d, public Vertex
{
public:
     virtual void fun1(){ };
     virtual void fun2(){ };
     void fun3(){ };
protected:
     float mumble;
};
下图是VS2010中 class Point2d的内存布局(虚继承):

  • 观察class Point2d的内存布局,
      存在虚函数,增加vfptr,将其放入表中
Point2d::$vftable@:
        | &Point2d_meta  //第一次声明
        |  0                    //偏移量
0      | &Point2d::fun1     //函数名
下图是VS2010中 class Point3d的内存布局(虚继承):

  • 观察class Point2d和Point3d的内存布局,
      可知,VS2010编译器中对于虚继承,是在 直接复制模型的基础上,选择了virtual base class table模型:即增加了一个vbptr。
      观察到Point3d中 vbptr指向的virtual base class table如下:
Point3d::$vbtable@:
 0      | 0
 1      | 8 (Point3dd(Point3d+0)Point2d)
      偏移量 0是相对整个derived class而言,而8是该vbptr相对于virtual base Point2d的位置。
      此外注意到,直接复制(继承)的 vfptr应该进行修正。
      Point3d::$vftable@:偏移量为 -8,即指向base Point2d的vfptr位置。
下图是VS2010中 class Vertex的内存布局(虚继承):

  • 观察class Point2d和Vertex的内存布局,
      可知,VS2010编译器中 对于虚继承直接复制后,增加了一个vbptr。
      观察到Vertex中 vbptr指向的virtual base class table如下:
Vertex::$vbtable@:
0      | -4
1      | 8 (Vertexd(Vertex+4)Point2d)
      偏移量 -4是相对整个derived class而言,而8是该vbptr相对于virtual base Point2d的位置。

      对于新增的虚函数:添加新的 vfptr(注:放在vbptr之前)
Vertex::$vftable@Vertex@:
        | &Vertex_meta //第一次声明
        |  0
0      | &Vertex::fun2
      此外注意到,直接复制(继承) vfptr应该进行修正。
      Vertex::$ vftable@Point2d@:偏移量为 -12,即指向base Point2d的vfptr位置。
下图是VS2010中 class Vertex3d的内存布局(虚继承):

  • 观察class Point3d、Vertex和Vertex3d的内存布局,
      对于虚继承,是在 直接复制模型的基础上,virtural base class Point2d只 复制一份
      注意到,
Vertex3d::$vftable@Vertex@:
        | &Vertex3d_meta
        |  0                    //Vertex3d::$vftable@Vertex@:偏移量为0,即指向base Vertex的vfptr位置。
 0      | &Vertex3d::fun2
Vertex3d::$vftable@:
        | -24                    //Vertex3d::$vftable@::偏移量为-24,即指向base Point2d的vfptr位置。
0      | &Vertex3d::fun1
      直接复制(继承)的 vfptr应该进行修正。
      注意到,
Vertex3d::$vbtable@Point3d@:
0      | 0
1      | 12 (Vertex3dd(Point3d+0)Point2d)
Vertex3d::$vbtable@Vertex@:
0      | -4        // Vertex3d::$vbtable@Vertex,偏移量-4是相对整个derived class而言,而20是该vbptr相对于virtual base Point2d的位置。
1      | 20 (Vertex3dd(Vertex+4)Point2d)

6.指向Data Members的指针

如何区分一个“没有指向任何data member”的指针和一个指向“第一个data member”的指针?

float Point3d::*p1 = NULL;
float Point3d::*p2 = &pointed::x;

每一个真正的member的offset值都被加上1。(为此,在真正使用该值去指出一个member之前,需先减掉1。)

如何通过origin.z计算origin的起始地址?

取一个nonstatic data member的地址,将得到它在class中的offset;
取一个绑定于真正class object身上的data member的地址,将会得到该member在内存中的真正地址。
因此 , &origin.z所得结果 - z的偏移量(相对应origin的起始地址) + 1 = origin的起始地址

参考资料及推荐资料:


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值