深度探索C++对象模型——Data语意学
一个空class与继承的相关讨论
class X {}; //sizeof(X)的大小为1
class Y: public virtual X {};//sizeof(Y)的大小为8
class Z: public virtual X {};//sizeof(Z)的大小为8
class A: public Y,public Z {};//sizeof(A)的大小为12
1)X的大小为1的原因:空class并不是空的,编译器给其存放由1 byte大小的char;
2)Y、Z大小为8的原因:其大小与机器有关;
- 语言本身造成的额外负担:有一个执行virtual base class的指针4byte
- 编译器对特殊情况所提供的优化处理
- Alignment的限制:对齐的限制。本来Y和Z大小为5 byte,编译器对其进行补齐,补上了3 byte,成为8 byte。
有的编译器会对empty virtual base class进行特殊优化,不加上那1 byte的char。这式上图的1,3 byte 就没有了,Y、Z的大小为4 byte。
3)A的大小为12的原因:一个virtual base class subobject 只会在derived class中存在一份实例。
3.1 Data Memeber 的绑定
3.2 Data Member 的布局
Nonstatic data memeber 在class object 中的排列顺序将和其被声明的顺序一样;任何介入其中的 static data member都不会被放进对象布局中。static data member是放在data segment中的,与个别class object无关。
C++标准要求,在同一个access section (也就是private、public、protected)中,members的排列只需要符合“较晚出现的members在class object中有较高的地址”。边界调整、对象内部的data member(vptr)等会介于声明的members之间。C++允许多个access section之中的data member 只有排列。
class Point3d { //xyz在class object 中的顺序与声明一样
public:
...
private:
float x;
static List<Point3d*> *freeList;
float y;
static const int chunkSize = 250;
float z;
};
class Point3d1 { //该class的大小与上一个一样;
public: //xyz在class object 中的顺序与编译器有关
...
private:
float x;
static List<Point3d*> *freeList;
private:
float y;
static const int chunkSize = 250;
private:
float z;
};
3.3 Data Memeber的存取
- Static Data Member:
只有一个实例,每一个member的存取许可以及与class关联没有额外的空间和执行时间上的额外负担;
通过一个指针和一个对象调用static data member是一样的;static data member并不在class中,因此存取static member 并不需要通过class object;
对一个static data member取地址只能得到一个指向其数据类型的指针;
如果有多个class都定义了同样的static data member编码,编译器要对其进行编码,以获得第一无二的识别代码。
Point3d one;
Point3d* pt=&one;
one.x = 0.0;
pt->x = 0.0;
&Point3d::chunkSize;//得到的是const int*指针
- Nonstatic Data Members:
nonstatic data member存放于class object中,只能有显示或隐式class object调用,这里的隐式class object是指this指针;
想要对一个nonstatic data member进行存取操作,编译器需要把class object的起始地址加上data member的偏移位置(offset)。
指针调用一个virtual base class 的nonstatic data member的效率会稍慢。
3.4 继承与Data Member
在C++继承模型中,一个derived class object所表现出来的东西是其自己的members加上其base class(es) members 的总和。
base class和derived class成员的顺序是没有定义的,和编译器有关,一般来说是base class先出现。
只要继承不要多态(Inheritance without Polymorphism)
即单一继承不含有虚函数;
把一个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;
};
Concrete3 占16 bit,Concrete1 有3 bit 的边界调整;Concrete2 有3 bit 的边界调整;Concrete3 有3 bit 的边界调整.
如果没有“出现在derived class中的base class subobject 有其完整的原样性”话,让将指向derived class 指针的类对象赋值给 base class 对象会导致derived class 成员被破坏。
加上多态(Adding Polymorphism)
加入多态后,对 class 带来空间可存取时间上额外的负担
- 导入了一个virtual table,用来存放class所声明的每一个virtual functions的地址,这个表的元素的个数一般是被声明虚函数个数加上一个或两个slots行(用于支持runtime type identification);
- 在每一个class object 中导入一个vptr(虚函数表指针),提供执行期的链接,是每一个object能够找到相应的virtual table;
- 加强constructor,使其能够为vptr设定初值,指向对于class的virtual table;
- 加强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进行支持;
多重继承(Multiple )
一个多重继承的例子
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;
pv=&v3d; //需要将该派生类v3d的地址加上它前面的base的大小的偏移量。
p2d=&v3d;//p2d的类型为Point2d,为最左边的基类的基类,其地址与Point3d相同
//,只需要进行简单的复制即可
p3d=&v3d;//p3d的类型为Point3d,为最左边的基类,只需要进行简单的复制即可
虚拟继承(Virtual Inheritance)
一个虚拟继承的例子
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;
};
编译器实现虚拟继承的方法,将其分成两个部分:一个不变区域,一个共享区域。不变区域可以直接使用,计算offset即可;共享区域,即virtual base class subobject ,采取间接存取。
虚拟继承会导致的问题: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 的偏移。
3.5 对象成员的效率
虚拟继承的效率比较差。
3.6 指向Data Member的指针
vptr在编译器中不是放在对象的头部就是在对象的尾部。
虚拟继承妨碍编译器优化的有效性。