Data语意学

  先看一段代码

class X {};
class Y : public virtual X {};
class Z : public virtual X {};
class A : public Y, public Z {};
 
// sizeof(X)的结果为1
// sizeof(Y)的结果为8
// sizeof(Z)的结果为8
// sizeof(A)的结果为12

  实际上,class X并不为空,它被编译器安插了一个隐藏的1 byte,这样使得class X的每个object在内存中拥有不同的地址。但是为什么class Y,class Z的sizeof大小是8呢?

  class Y,class Z的大小受到三个因素的影响:

  1. 语言本身造成的额外负担:当语言支持virtual base classes特性时,就会需要一些额外的负担来实现,这表现在某种形式的pointer上,在32位机器上,指针占有4 bytes,它或指向virtual base class object,或指向一个相关的表格,该表格中存放的若不是virtual base class object的地址,就是偏移位置(offset)。
  2. 编译器对于特殊情况提供的优化处理:virtual base class object的1 bytes也出现在class Y,class Z中,传统上它被放在derived class的尾部部分。
  3. Alignment的限制:class Y,class Z的大小本应为5 bytes,但为了使在内存中更有效的存取,会将其调整为某数的整数倍,在32位机器上调整为4 bytes的整数倍。

  某些编译器(例如我用的vs2013)为我们提供了对empty virtual base class的特殊支持,就是将一个empty virtual base class视为derived class object最开头的一部分,也就是说derived class现在有member了,它不再是空了,也就不需要编译器安插那1 byte,自然也就不必为alignment padding提供3 bytes了。

  现在它们的格局是上图这样的了。class Y,class Z的sizeof就是4 bytes了,当然class X还是一个empty class编译器依然会安插1 byte。那么class A的sizeof又会是8 bytes,因为它不存在base class subobject那1 bytes了。

  对于virtual inheritance(虚拟继承)而言,在一个derived class中只会存在一份virtual base class subobject,因此,class A的大小由以下几点决定:

  1. 被大家共享的唯一一个class X实例,大小为1 byte;
  2. base class Y大小减去“因virtual base class X而配置”的4 bytes,剩下的大小为4 bytes;base class Z同理;
  3. class A自己的大小为0
  4. class A的Alignment边界调整,4+4+1=9,做调整后为12

  通过上面三点,可知class A的大小为12 bytes了。

  c++ Standard并不强制规定“base class subobject的排列顺序”或“不同存取层级的data members的排列顺序”。data members是整个class在程序执行时的某种状态,nonstatic data members放置的是“个别的class object”感兴趣的数据。

一、Data Member的绑定

  早期的c++程序设计风格:

  1. 把所有的data members放在class 声明起头处,以保证正确的绑定
  2. 把所有的inline functions不管大小都放在class声明外

  一个inline函数实体,在整个class声明未被完全看见前,是不会被评估求值的。如果inline在函数体内部,那么对函数的分析直至class声明的右大括号出现才开始。所以在inline member function躯体之内的一个data member绑定操作,会在整个class声明后才发生。即:

  1. inline member functin躯体之内的一个data member绑定操作会在整个class声明完成之后才发生。
  2. argument list中的名称还是会在它们第一次遭遇时被适当地决议。
  3. 为避免错误,早期出现三种防御性代码风格,把data members放在class声明开始处,把inline functions放在class声明之外,把nested type声明放在class的起始处。

二、Data Member的布局

  1. static data members放在程序的data segment中,和class objects无关。
  2. 同一个access section中的data members的排列符合较晚出现的data members在class object中有较高的地址(因为可能存在边界调整导致地址不连续)。
  3. 允许多个access section中的data members自由排列(但是没有编译器这么做)。
  4. derived class members和base classes members可以自由排列。
  5. 在大部分编译器上头,base class members总是先出现,但属于virtual base class的除外。
  6. 编译器内部产生出来的data members可自由地放在任何位置上。

三、Data Member的存取

  1. 通过对象、引用或指针存取一个static data member的成本是一样的,存取不会招致任何空间上或时间上的额外负担(因为member不在class object中,所以存取不需通过object)。
  2. 通过对象、引用或指针存取一个nonstatic data member的成本,其效率和存取一个C struct member或一个nonderived class的member(单一继承或多重继承)是一样的(多重继承的时候,members的位置在编译时就固定了,存取members只是一个简单的offset运算)。
  3. 通过引用或指针比通过对象存取一个virtual base class的member的速度会稍慢(因为不知道这个引用或指针指向哪一种class type,也就不知道编译期这个member真正的offset的位置,所以这个存取操作必须延迟至执行期,经由一个额外的间接导引,才能够解决)。

  若取一个static data member地址,会得到得到数据类型的指针,而不是指向class member的指针,因为static member并不在任何一个class object。

  若存取nonstatic data member,编译器需要把class object的起始地址+data member的偏移位置(offset)

origin.y=0.0;

//那么&origin.y将等于
&origin+(&Point3d::y-1);

  指向data member的指针,其offset值总是被加上1,这样编译器可以区分出“一个指向data member的指针,用以指向class的第一个member”和“一个指向data member的指针,没有指向任何member”两种情况。

四、“继承”与Data Member

  一个子类对象所表现出来的东西是它自己的成员变量和它的基类的成员变量的总和。在derived class member和base class member的排列顺序,c++ standard并未强制指定,大部分编译器总是保证base class member先出现,但是virtual base class时除外(任何一条通用规则遇到virtual base class都除外)。

1.只要继承不要多态

  一般而言,具体继承(相对于虚继承),并不会增加空间或存取时间上的额外负担。

  把一个class分成两层或更多层,而膨胀了其所需的空间;出现在derived class中的base class subobject 有其完整的原样性;
    一个例子说明:

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

  其占8 bit,有1比特的边界调整

  把其拆分为三个层次结构后

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

  sizeof(Concrete3) = 16的原因:Concrete1占8个字节(对齐占3字节),Concrete2占4字节(对齐占3字节),Concrete3占4字节(对齐占三字节)。

  如果Concrete1的char放入之后,不对齐,后面直接放Concrete2的char和Concrete3的char

Concrete2 *pc2;
Concrete1 *pc1_1, *pc1_2;

pc1_1 = pc2; //令pc1_1指向Concrete2对象中的Concrete1子对象
*pc1_1 = *pc1_2; //pc1_2指向的Concrete1对象赋给Concrete1子对象
//它会复制8个字节过去,那么Concrete2对象中的bit2,就会被覆盖产生了非期望值

  如果没有“出现在derived class中的base class subobject 有其完整的原样性”话,让将指向derived class 指针的类对象赋值给 base class 对象会导致derived class 成员被破坏。

2.加上多态

  加入多态后,对 class 带来空间可存取时间上额外的负担

  1. 导入了一个virtual table,用来存放class所声明的每一个virtual functions的地址,这个表的元素的个数一般是被声明虚函数个数加上一个或两个slots行(用于支持runtime type identification);
  2. 在每一个class object 中导入一个vptr(虚函数表指针),提供执行期的链接,是每一个object能够找到相应的virtual table;
  3. 加强constructor,使其能够为vptr设定初值,指向对于class的virtual table;
  4. 加强destructor,使其能够析构“指向对于class的virtual table” 的vptr;
class Point2d{
public:
    Point2d(float x = 0.0,float y = 0.0):_x(x),_y(y);
    //...
    virtual float z(){return 0.0;}
    virtual void z(float){}
    virtual void operator+=(const Point2d& rhs)
    {
        _x+=rhs.x();
        _y+=rhs.y();
    }
    //....
protected:
    float _x,_y;
};
class Point3d : public Point2d {
public:
    Point2d(float x = 0.0,float y = 0.0,float z = 0.0):_x(x),_y(y);
    //...
    float z(){return _z;}
    oid z(float newz){_z=newz;}
    virtual void operator+=(const Point2d& rhs)
    {
        Point2d::operator+=(rhs);
        _z+=rhs.z();
    }
    //....
protected:
    float _z;
};

  Point2d 多了一个指向虚函数表的指针,相应的构造函数,析构函数也需要对vptr进行支持

2.1vptr放在class的尾端

  放在尾端时可以实现对struct的继承,如下

struct no_virts
{
    int d1,d2;
};
class has_virts:public no_virts
{
public:
    virtual void foo();
    //...
private:
    int d3;
};
no_virts *p=new has_virts;

  vptr放在尾端可以保留base C struct的对象布局,因而允许在c程序代码中也能使用。

2.2vptr放在class开头

  为了支持虚继承和抽象基类,某些编译器开始把vptr放在class object的开头。但是此时会丧失与C语言的兼容性。

  把vptr放在class object的前端,对于“在多重继承之下,通过指向class members的指针调用virtual function”,会带来帮助,否则不仅“从class object起始点开始量起”的offset必须在执行期准备妥当,甚至与class vptr之间的offset也必须准备妥当。

3.多重继承

  从上图可以得知base class和derived class的object都是从相同的地址开始的,所以当一个derived class object指定给base class(不论继承有多深)的指针或引用,这个操作并不需要编译器去调停或修改地址。

  如果base class没有virtual function二derived class有,那么单一继承的自然状态会被打破,此时把一个derived object转换为其base类型,需要编译器介入,用以调整地址。

  多重继承复杂度在于derived class和上一个base class乃至于上上一个base class。。。之间的“非自然”关系。

class Point2d{
public:
    ....    //(拥有virtual接口,所以Point2d对象中会议vptr)
protected:
    float _x,_y;
};
class Point3d : public Point2d {
public:
    .... //
protected:
    float _z;
};
class Vertex{
public:
    ...  //(拥有virtual接口,所以Point2d对象中会议vptr)
protected:
    Vertex *next;
};
class Vertex3d : public Point3d, public Vertex
{
public:
    ...  //(拥有virtual接口,所以Point2d对象中会议vptr)
protected:
    float mumble;
};

  对于一个多重派生对象,将其地址付给最左边(也是第一个)base class的指针时,和单一继承相同,只需要地址复制就可以,因为二者指向相同的起始地址;如果想要将一个多重派生类对象的地址赋值给第二个或者后面其他的base class时,则需要将地址进行修改(偏移):加上(或者减去,如果是downcast的话)介于两者之间的base class subobject(s) 大小。

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

//需要将该派生类v3d的地址加上它前面的base的大小的偏移量。
pv=&v3d;
//需要如下转化
pv=(Vertex*)(((char*)&v3d)+sizeof(Point3d));

//对于如下的转化只需要地址拷贝就好
p2d=&v3d;//p2d的类型为Point2d,为最左边的基类的基类,其地址与Point3d相同
         //,只需要进行简单的复制即可
p3d=&v3d;//p3d的类型为Point3d,为最左边的基类,只需要进行简单的复制即可

//对于下面的指定操作
pv=pv3d;
//需要如下转化,防止pv3d为0
pv=pv3d?(Vertex*)((char*)pv3d)+sizeof(Point3d):0;

  对于reference,不需要对可能的0值做防卫,因为reference不可能做参考到“无物”。

  C++standard未规定vertex3d中base classes Point3d和vertex有特定的排列顺序,原始的cfront编译器根据声明顺序来排序他们,目前各种编译器任然以此方式完成多重base classes的布局(如果加上虚继承就不一样)。

  如果要存取第二个(或后继)base class中的一个data member,不需要付出额外的成本,members的位置在编译时就固定了,因此存取一个members只是一个简单的offset运算,就像单一继承一样,不论是由reference或是一个object来存取。

4.虚拟继承

  如果一个class内有一个或多个virtual base class subobject,将会被分割为两个部分:一个不变区和一个共享区,不变区中的数据不管后继如何变化,总是有固定的offset(从object开头),这一部分可以被直接存取;共享区表现的就是virtual base class subobject,这一部分的数据,其位置会因每次派生操作而改变,他们只可以被间接存取。

class Point2d{
public:
    ....    //(拥有virtual接口,所以Point2d对象中会议vptr)
protected:
    float _x,_y;
};
class Point3d : public virtual Point2d
{
public:
    .... //
protected:
    float _z;
};
class Vertex :public virtual Point2d
{
public:
    ...  //(拥有virtual接口,所以Point2d对象中会议vptr)
protected:
    Vertex *next;
};
class Vertex3d : public Vertex, public Point3d
{
public:
    ...  //(拥有virtual接口,所以Point2d对象中会议vptr)
protected:
    float mumble;
};

  一般是先安排好不变区的部分,再建立共享区的部分。

  虚拟继承会导致的问题:

  1. 对每一个对象必须对其每一个virtual base class会多一个指向virtual base class的指针
  2. 虚拟继承串联加长会导致间接存取层次增加

  解决办法:

  1. Microsoft引入一个virtual base class table,每个class object如果有一个或者多个virtual base class ,就引入一个指针,指向virtual base class table。
  2. 在virtual function table 放置 virtual base class 的偏移。virtual function table可能由正值或负值来索引,如果是正值就是virtual functions,如果是负值,则是索引到virtual base class offset。

  对于virtual base class最好的运用形式就是一个抽象的virtual base class,没有任何的data member。

五、对象成员的效率

  虚拟继承效率较差,编译器无法识别出“虚拟继承而来的data member”的存取是通过一个非多态类对象(因而不需要执行期的简介存取)进行的,两个编译器都会对双层虚拟继承产生间接存取,“间接性”压制了“把所有运算都移往寄存器执行”的优化能力,但是严重影响非优化程序的执行效率。

六、指向Data Member的指针

  为了区分一个“没有指向任何data member”的指针和一个指向“第一个data member的指针”,如下

class Point3d { 
public: 
 virtual ~Point3d(); 
 // ... 
protected: 
 static Point3d origin; 
 float x, y, z; 
};

& Point3d::z; //将得到z在class object 中的offset
printf("%p", &Point3d::z); //c,不能用cout

& Point3d::z; //float Point3d::*
& origin.z //float*

float Point3d::*p1=0;
float Point3d::*p2=&Point3d::x;

  每一个真正的member offset值都被加上1,因此真正使用该值指出一个member之前,先减1。

总结:

  1. 取一个static data member的地址得到的将是它在内存中的地址,类型是一个指向其数据类型的指针,而不是指向其class member的指针。
  2. 取一个nonstatic datamember的地址,将会得到它在class中的offset,类型是一个指向其class member的指针。
  3. 取一个绑定于真正class object身上的data member的地址,将会得到该member在内存中的真正地址,类型是一个指向其数据类型的指针。

posted on 2019-12-27 21:32  tianzeng  阅读(288)  评论(0编辑  收藏  举报

导航