第3章 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

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

3.1 Data Member的绑定

member scope resolution rules:如果一个inline函数在class声明之后立刻被定义的话,那么还是对其评估求值,例如:

extern int x;

class Point3d
{
public:
	//对于函数本身的分析将延迟直至
	//class声明的右大括号出现才开始
	float X() const {return x;}
private:
	float x;
}
//事实上,分析在这里进行

3.2 Data Member的布局

class Point3d
{
private:
	float x;
	static List<Point3d*> *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的最后

3.3 Data Member的存取

3.3.1 Static Data Members

每个静态成员变量只存在一个实体,存在于data segment中。即使它是从一个复杂继承关系中继承而来的member,它也是只有一个实体。

PS:若取一个static data member的地址,会得到一个指向其数据类型的指针,而不是一个指向其class member的指针,因为static data member并不内含一个class object中

PS1:如果有两个class,都声明了同名的static member,它们都会被放进data segment中,会导致命名冲突。编译器此时会暗中对每一个静态成员变量编码,这种手法称为name-mangling。

3.3.2 Nonstatic Data Members

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);

3.4 “继承”与Data Member

3.4.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数值未知

用图形解释如下:

3.4.2 加上多态

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语言的兼容性。

3.4.3 多重继承

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,因此不需要测试。

3.4.4 虚拟继承

在虚继承中,不同编译器针对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;

3.5 对象成员的效率

3.6 指向Data Member的指针

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的起始地址。

3.6.1 指向Members的指针的效率问题

猜你喜欢

转载自blog.csdn.net/sinat_25394043/article/details/81457882
今日推荐