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
X并非是空的,会有一个隐晦的1 byte,那是被编译器放进去的一个char,目的是让该class的两个object得以在内存中配置独一无二的地址
Y Z的大小主要受三个因素影响:
1.语言本身所造成的额外负担:虚函数、虚继承会出现vptr,为4 byte
2.编译器对于特殊情况所提供的优化处理:X的1 byte也会出现在Y Z,某些编译器会对empty virtual base class作特殊处理
3.Alignment的限制:大小为4 byte的倍数
下图为XYZ的布局关系图:
A的大小由下面几点决定:
1.X的1byte
2.Y,Z的大小4 byte
3.A的大小0
4.alignment填补3 byte
member scope resolution rules:如果一个inline函数在class声明之后立刻被定义的话,那么还是对其评估求值,例如:
extern int x;
class Point3d
{
public:
//对于函数本身的分析将延迟直至
//class声明的右大括号出现才开始
float X() const {return x;}
private:
float x;
}
//事实上,分析在这里进行
class Point3d
{
private:
float x;
static List *freeList;
float y;
static const int chunkSize = 250;
float z;
}
nonstatic data members在class object中的排列顺序将和其被声明的顺序一样,在上面的例子中就为x y z
static data members则存放在data segment中。
C++标准要求中,在同一个access section(即private,public,protected等区段中)中,members的排列只需符合“较晚出现的members在class object中有较高的地址”这一条件即可。也就是说各个members不一定必须要连续排列,alignment可能会穿插其中。
PS:class object中可能会出现的vptr通常会被放在所有明确声明的members的最后
每个静态成员变量只存在一个实体,存在于data segment中。即使它是从一个复杂继承关系中继承而来的member,它也是只有一个实体。
PS:若取一个static data member的地址,会得到一个指向其数据类型的指针,而不是一个指向其class member的指针,因为static data member并不内含一个class object中
PS1:如果有两个class,都声明了同名的static member,它们都会被放进data segment中,会导致命名冲突。编译器此时会暗中对每一个静态成员变量编码,这种手法称为name-mangling。
nonstatic data members直接存放在每一个class object中,除非经由明确的(explicit)或暗喻的(implicit)class object,否则没有办法存取他们。只要在类中处理非静态成员变量,那么必然会引发“implicit class object”
Point3d Point3d::translate(const Point3d &pt)
{
x += pt.x;
y += pt.y;
z += pt.z;
}
上述代码在编译器中实际发生的操作如下所示:
Point3d Point3d::translate(Point3d *const this ,const Point3d &pt)
{
this->x += pt.x;
this->y += pt.y;
this->z += pt.z;
}
欲对一个nonstatic data member进行存取操作,编译器需要把class object的起始地址加上data member的偏移量(offset),例如
origin._y = 0.0;
//地址&origin,_y将等于
&origin + (&Point3d::_y - 1);
class Point2d
{
public:
Point2d(float x = 0,float y = 0):_x(x),_y(y){};
float x(){return x;}
float y(){return y;}
private:
float _x,_y;
}
class Point3d : public Point2d
{
public:
...
protected:
float _z;
}
上面的例子中就是在简单的继承关系且无虚函数的情况下,两个class object的数据分布情况。
其中容易出现的错误有:
1.重复设计一些相同操作的函数
2.把一个class分解成过多层,可能会为了显示抽象化而增大内存空间。
接下来介绍一下一个class及其派生来在内存中分布的具体过程:
class Concrete1
{
public:
..
private:
int val;
char bit1;
}
class Concrete2: public Concrete1
{
public:
..
private:
char bit2;
}
class Concrete3 :public Concrete2
{
public:
..
private:
char bit3;
}
现在来看看每个class的大小,
Concrete1:val 4 + bit1 1 = 5 byte,另外还有3 byte的alignment = 8 byte
Concrete2; Concrete1的8 byte + bit2 1byte + 3byte alignment = 12 byte
Concrete3; Concrete2的12 byte + bit3 1byte + 3byte alignment = 16 byte
下图为三个class在内存中的布局:
PS:这里可能会有人问了,为什么Concrete2中的Concrete1是8byte,而不是val,bit1的5byte,可以不要那3byte的alignment,同样的问题也可以用于Concrete3中的Concrete2,这样Concrete1 2 3的大小都是8byte,针对这个问题的解答如下:
Concrete1 *pc1_1, *pc1_2;
Concrete2 *p2;
pc1_1 = p2;
//p2部分的derived class subobject部分被sliced
//现在bit2 member现在有了一个并非预期的数值
*pc1_2 = *pc1_1; //bit2数值未知
用图形解释如下:
class Point2d
{
public:
Point2d(float x = 0,float y = 0):_x(x),_y(y){};
float x(){return x;}
float y(){return y;}
virtual float z() {return 0.0;}
virtual void operator+=(const Point2d& rhs)
{_x += rhs.x(); _y += rhs.y();}
private:
float _x,_y;
}
void foo(Point2d &p1, Point2d &p2)
{
p1 += p2;
}
上面代码中p1,p2可能是2d也可能是Point3d,这样的话程序运行时会判断具体是哪个class,因此会带来时间和空间上的额外负担:
1.每个class object中导入一个vptr,提供执行期的链接
2.导入一个和Point2d相关的vbtl,存放每个虚函数的地址
3.加强Constructor,使得它能够对vptr初始化
4.加强destructor,销毁vptr
在早先的C++中,vptr通常放在class object的尾端,这样可以保留base class C struct的对象布局,因而允许在C程序中也能使用。
后来,vptr更多是放到class object的首端,这样做好处在于在多重继承之下,通过指向class members的指针能够直接调用虚函数。如果还放在尾端,还需要在执行期确定从class object起始点开始的offset,当然这也丧失了C语言的兼容性。
class Point2d
{
public:
..//拥有virtual接口,所以2d对象中会有vptr
protected:
float _x,_y;
}
class Point3d : public Point2d
{
public:
//
protected:
float _z;
}
class Vertex
{
public:
..//拥有virtual接口,所以2d对象中会有vptr
protected:
Vertex *next;
}
class Vertex3d: public Point3d, public Vertex
{
public:
..//拥有virtual接口,所以2d对象中会有vptr
protected:
float mumble;
}
对于一个多重派生对象,将其地址指定给“最左端base class的指针”,情况和单一继承时相同,因为二者都有相同的起始地址。
至于第二个或后继的base class的地址指定操作,则需要将地址修改,加上或减去介于中间的base class subobject大小。例如:
Vertex3d v3d;
Vertex *pv;
Point2d *p2d;
Point3d *p3d;
pv = &v3d;
//内部转化伪代码
pv = (Vertex*)(((char*)&v3d) + sizeof(Point3d));
再比如:
Vertex3d *pv3d;
Vertex *pv;
pv = pv3d;
//错误的内部转换
pv = (Vertex*)((char*)pv3d)) + sizeof(Point3d);
//正确的形式
pv = pv3d ? (Vertex*)((char*)pv3d)) + sizeof(Point3d) : 0;
错误的原因是如果pv3d为0,pv将获得sizeof(Point3d)的值,这显然是错误的。因此需要一个条件测试。
至于引用, 因为引用不可能为0,因此不需要测试。
在虚继承中,不同编译器针对virtual base class subobject会采取不同的操作,因为virtual base class subobject的位置在不同的派生操作下会产生变化。下面分别介绍不同编译器在这种情况下都是如何处理class中的共享部分的:
比如我们有如下继承体系:
(1)cfront编译器会在每一个派生类对象中安插一些指针,每个指针指向一个虚基类,要取得继承而来的virtual base class members,就可以使用这些指针来完成。例如:
void Point3d::operator+=(const Point3d &rhs)
{
_x += rhs._x;
_y += rhs._y;
_z += rhs._z;
}
//内部转换伪代码
_vbcPoint2d->_x += rhs._vbcPoint2d->_x; //vbc意思是virtual base class
_vbcPoint2d->_y += rhs._vbcPoint2d->_y;
_z += rhs._z;
Point2d *p2d = pv3d;
Point2d *p2d = pv3d ? pv3d->_vbcPoint2d : 0;
cfront编译器这样处理的缺点:
1.每个对象都得针对每一个虚基类而多背负一个指针,这样的话class object大小会因为虚基类的变化而变化,不固定
2.由于虚继承串链的加长,导致间接存取层次的增加,这样的话存取时间会因为继承深度的增多而增多。
(2)MetaWare编译器针对cfront处理的第二个缺点,解决方法是经由拷贝操作来取得所有的nested virtual base class的指针,放到派生类中,这样存取时间就是固定的了,但是空间也会变大。用图形解释见下图:
(3)Microsoft编译器针对cfront处理的第一个缺点,解决方法是引入virtual base class table,安插一个指针指向这个table,table中存放的是那些指向虚基类的指针。
(4)Sun编译器针对cfront处理的第一个缺点,还有一种解决方法是在虚函数表中放置offset,将virtual base class offset和virtual function entries混杂在一起。虚函数中通过正负值索引,若为正值,获取的是虚函数,若为负值,获取的是virtual base class offset。
利用索引的例子:
(this + _vptr_Point3d[-1])->x += (&rhs + rhs._vptr_Point3d[-1])->x;
(this + _vptr_Point3d[-1])->y += (&rhs + rhs._vptr_Point3d[-1])->y;
_z += rhs._z;
class Point3d
{
public:
virtual ~Point3d();
protected:
static Point3d origin;
float x,y,z;
}
现在,取某个坐标成员的地址:&Point3d::z
这将得到z在class object中的偏移量(offset),最小值为x y大小的总和,因为C++要求要将同一access level的members的排列次序应和声明次序相同。
vprt通常放在object的首尾两处,若在尾端,xyz的offset分别为0,4,8,若在头处,offset又为4,8,12,但是实际情况是1,5,9或5,9,13。这是为什么?
要解答这个问题首先考虑,如何区分一个“没有指向任何data member的指针“和一个指向”第一个data member”的指针?
float Point3d::*p1 = 0;
float Point3d::*p2 = &Point3d::x;
//Point3d::*意为指向Point3d data member的指针类型
if(p1 == p2)
{
//p1 p2 contains the same value
//threy must address the same member
}
为了区分p1 p2,每一个真正的member offset都被加上1。
下面再看这个例子:
&Point3d::z;
Point3d origin;
&origin.z;
上面代码第一行,取一个nonstatic data member的地址,得到的是它在class中的offset
第三行,取一个绑定于真正class object身上的data member,将会得到该member在内存中的真正地址。第三行所得值减去z的偏移值再加1就得到origin的起始地址。