`
xitonga
  • 浏览: 586004 次
文章分类
社区版块
存档分类
最新评论

深度探索C++对象模型 3Data语意学

 
阅读更多

第三章Data语意学

已知如下程序:

#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(){

cout << sizeof(X) << endl;

cout << sizeof(Y) << endl;

cout << sizeof(Z) << endl;

cout << sizeof(A) << endl;

return 0;

}

在VS2008上执行结果如下:

一个空的class如X,事实上不是空的。因为空类也可以被实例化,就必须被分配内存,所以编译器默认分配给他一个字节(安插进去了一个char,故大小为1byte)以便标记初始化的类实例,同时使空类占用的空间也最少。

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

1.语言本身所造成的额外负担:当语言支持virtual base classes或virtualfunction时,就会造成额外负担。这里Y和Z上都增加了一个指针(大小为4bytes),指向virtual base class X subobject。

2.编译器对于特殊情况所提供的优化处理:有些传统编译器会将virtual base class X subject的1byte大小放在X和Y上,而有些编译器(如VS2008,VC++ 6.0)因为Y和Z已经有一个指针的大小指向virtual baseclass X subject,可以认为Y和Z不是空类,就不需要编译器来生成1byte。

3.Alignment的限制:alignment就是将数值调整到某数的整数倍。在32位计算机上,通常是4bytes(32位),以使bus的运输量达到最高效率。

所以Y和Z的大小在VS2008上为4字节。如下图所示:

而如果这段程序在传统编辑器上,则大小为8字节(4字节的指针+1字节+3字节的Alignment填充),如下图所示:

对于class A,在传统编辑器上大小由以下几点决定:

1.被大家共享的唯一一个class X实例,大小为1byte。

2.Baseclass Y的大小,减去“因virtual base class X而配置”的大小,结果是4byte。Base classZ也一样。加起来是8bytes。

3.Class A自己的大小:0byte

4.Class A的alignment数量,前面三项加起来是9bytes,故需要填充4bytes。结果为12。

但在VS2008上,class X的1byte被拿掉,故无需额外的3byte填充,所以大小为8bytes。

C++ Stand并不强制规定如“base classsubject的排列顺序”或“不同存取层级的data members的排列顺序”,也不规定virtualfunction或virtual base class的实现细节,这些实现都由编译器产商自定。

在这一章,class的data members以及class hierarchy是中心议题。对于nonstaticdata member(不论是否继承而来)都放置于每一个class object中,但没有定义他们之间的排列顺序。至于staticdata member,则被放置在程序的一个global data segmen中,只存在一份实例,不会影响class object的大小。

3.1DataMember的绑定

//某个foo.h头文件,从某处含入

extern float x;

//程序员的Point3d.h文件

class Point3d{

public:

Point3d( float,float,float);

float X() const{return x;}

void X(float new_x)const{x = new_x;}

private:

float x, y, z;

}

这里Point3d::X()返回的是class内部的x,但早期情况并不如此。和早期情况相似的是,现在的member function的argument list中的名称还是会在他们第一次遭遇决议时被适当的完成。如下所示:

#include<iostream>

#include <string>

using namespace std;

typedef string length;

class Point3d{

public:

void mumble(length val){ _val = val ;}

length mumble(){return _val;}

void prif(){cout << _val;}

private:

typedef float length;

length _val;

};

int main(){

Point3d p3;

return 0;

}

执行就会报错,原因是在两个member function signatures中length的类型都决议为global typedef,也就是string,不能将string转化为flaot或将flaot转化为string。

3.2Data Member的布局

Nonstaticdata members在class object中的排列顺序将和其被声明的顺序一样。C++Standard要求,在同一个access section(也就是private,protected,public)中,members排列顺序只要符合“较晚的members在class object中有较高的地址”这一条件就可以。也就是说各个members并不一定得连续排列。例如,members的边界调整(alignment)可能就需要补充一些bytes。此外,虽然目前的编译器不是把vptr放在class object的最前端就是放在显示声明的members的最后,但是C++ Standard并没有要求,它允许编译器把那些内部产生出来的members(如vptr)自由放在任何位置。

class Point{

private:

float x;

float y;

private:

float z;

}

按C++Standard,在每个Point object中,x必须在y前面,但不一定必须在z前面。但目前编译器都是将z放在y之后。

3.3 Data Member的存取

已知类Point3d如下:

class Point3d{

public:

float x;

static list<Point3d*> *freeList;

public:

float y;

static const int chunkSize;

public:

float z;

};

Point3dorigin, *pt=&origin;

Origin.x=0.0;

Pt-x= 0.0;

通过origin与通过pt存取有什么重大差异?可从static和nonstatic Data Members两方面看。

Static Data Members

对于static data member(无论是否继承而来,是否是虚基类中),不管通过对象读取还是通过指向对象的指针读取,无差异。

如:origin. chunkSize = 250;内部会被转化为 Point3d::chunkSize= 250;

Pt-> chunkSize=250;同样也会被转化为Point3d::chunkSize= 250;

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

&Point3d::chunkSize; //会获得如下地址const int*

Nonstatic Data Members

欲对一个nonstatic data member进行存取操作,编译器需要把class object的起始地址加上data member的偏移位置。举个例子:

如果

origin.y=0.0;

那么地址&origin.x将等于:

&origin+(&Pointd::y-1)

请注意其中的-1操作。指向data member的指针,其offset值总是被加上1,这样可以是编译器区分“一个指向data member的指着,用于指出class的第一个member”和“一个指向data member的指针,没有指出任何member”两种情况。如:

floatPoint3d::*p1=0;// 这个就是“没有指向任何Data member”的指针

floatPoint3d::*p2=&Point3d::x;// 这个就是指向x的“第一个Data member

//的指针”

每一个nonstatic data member的偏移位置在编译器即可获知,甚至如果member属于一个base class subobject也是一样。

但对于虚拟继承,将为“经由base class subobject”存取class members导入一层新的间接性,比如

Point3d*pt3d;

Pt->x= 0.0;

其执行效率在x是一个struct member,classmember、单一继承,多重继承的情况下都完全相同。但如果x是一个virtual base class的member,存取速度会稍慢一点。

所以下面两式:

Origin.x=0.0;

Pt-x= 0.0;

当Point3d是一个derived class,而起继承结构中有一个virtual base class,并且被存取的member是一个从该virtual base class继承而来的member时,就会有重大差异。这时候我们不能说pt必然指向哪一种class type(因此我们也就不能知道编译器时期这个member真正的offet位置),所以这个存取操作必须延迟至执行期,经由一个额外的间接导引才能解决。但如果使用origin,其类型无疑是Point3d class,而即使它继承自virtual base class,其member的offset位置也在编译期就固定了。

3.4“继承”与Data Member

在C++继承模型中,一个derived class object所表现的出来的东西,是其自己的members加上其base class members的总和。至于derived class members和base class members的排列顺序,则并未在C++ Standard中强制指定;理论上编译器可以自由安排之。在大部分编译器上头,base class members总是先出现,但属于virtual base class的除外(一般而言,任何一条通则一旦碰上virtual base class就没辙了,这里也不例外)

定义一个class,没有虚函数,也没有继承,其数据分布和struct一模一样。

class Point2d{

public:

private:

float x,y;

};

class Point3d{

public:

private:

float x,y,z;

};

这节从单一继承且不含virtual function、单一继承并含virtual function、多重继承和虚拟继承四种情况讨论:

1. 单一继承且不含virtual function

class Point2d{

public:

Point(float x=0.0,floaty=0.0)

:_x(x),_y(y){};

float x(){return _x;}

float y(){return _y;}

void(float newX){_x=newX; }

void(float newY){_y=newY; }

void operator+=(const Point2d &rhs){

_x += rhs.x();

_y += rhs.y();

}

protected:

float _x,_y;

};

class Point3d:publicPointed2d{

public:

Pointed(float x = 0.0,float y=0.0, float z=0.0)

:Point3d(x,y),_z(z){};

float z(){ return_z;}

void z(float newZ){_z = newZ; }

void operator+=(const Point3d &rhs){

Point2d::operator +=(rhs);

_z += rhs.z();

}

private:

float z;

};

布局如下:

一般而言具体继承(相对于虚拟继承)并不会增加空间或存取时间上的额外负担。但把一个class分解成两层或多层,有可能会为了“表现class体系抽象化”而膨胀所需要的空间。

class Concrete{

private:

int val;

char c1;

char c2;

char c3;

};

占用8bytes,其布局如下:

现在如果将Concrete分为三层结构如下:

class Concrete1{

private:

int val;

char bit1;

}

class Concrete2 : publicConcrete1{

private:

char bit2;

}

class Concrete3 : publicConcrete2{

private:

char bit3;

}

则这里Concrete3需要16bytes,其布局分布如下:


2. 单一继承并含virtual function

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 +=(constPoint2d& rhs){

_x += rhs.x();

_y += rhs.y();

}

protected:

float _x,_y;

};

class Point3d:publicPointed2d{

public:

Pointed(float x = 0.0,float y=0.0, float z=0.0)

:Point3d(x,y),_z(z){};

float z(){ return_z;}

void z(float newZ){_z = newZ; }

void operator+=(const Point2d &rhs){//没有多态时,这里为constPoint3d &rhs

Point2d::operator +=(rhs);

_z += rhs.z();

}

private:

float z;

};

void foo(Point2d &p1,Point2d &p2){

p1 += p2;

}

以上foo函数中的参数p1和p2可能是2d也可能是3d,只是多态带来的弹性,但这对于Point2d class也带来了空间和时间上的额外负担:

1.导入一个和Point2d有关的virtual table,用来存放它所声明的每一个virtual function的地址。这个table的元素个数一把是被声明的virtual function的个数加上一个或两个slots(用以支持runtime type identification)。

2.在每个class object中导入一个vptr,提供执行期的链接,使每一个object能够找到相应的virtual table。

3.加强constructor,使他能够成为vptr的初值,让他指向class所对应的virtual table。这意味着在derived class和每一个base class的constructor中,重新设定vptr的值。

4.加强destructor,使他能够抹消“指向class的相关virtual table”的vptr。

在cfront编译器中,vptr被放在class object的尾端,用以支持下面的继承类型。

struct no_virts{

int d1,d2;

};

class has_virs:public no_virs{

public:

virtual void foo();

private:

int d3;

};


到了C++2.0,某些编译器(如VC)把vptr放在了class object的起头处。如下图所示:


把vptr放在class object的前端,对于“在多重继承之下,通过指向class member的指针调用virtual function”会带来一些帮助。

这里我们的Point2d和Point3d的布局如下(如果把vptr放到尾部的话):


3.多重继承

Point3dp3d;

Point2d*p = &p3d;

把一个derived class object指定给base class的指针或引用,这个操作不需要要编译器去调停或修改地址,会很自然的发生。但如果base class没有virtual function,而derived class有,那么单一继承的自然多态就会被打破。这种情况下,把一个derived object转换为其base类型,就需要编译器介入,用以调整地址(因为vptr的插入之故)。在既是多重继承有事虚拟继承的情况下,编译器的介入更有必要。

如:

class Point2d{

public:

//。。译注:拥有virtual接口,所以Point2d对象之中会有vptr

protected:

float _x,_y;

};

class Point3d:public Point2d{

public:

//...

private:

float _z;

};

class Vertex{

public:

//...译注:拥有virtual接口,所以Vertex对象之中会有vptr

private:

Vertex *next;

};

class Vertex3d:public Point3d,public Vertex{

public:

//...

private:

float mumble;

};

对于一个多重派生对象,将其弟子指定给“最左端base class的指针”,情况将和单一继承时相同,因为两者都指向相同的起始地址。需付出的成本只有地址的指定操作而已。至于第二个或后继的base class的地址的指定操作,则需要将地址修改过:加上介于中间的base object subobject大小,例如:

Vertex3d v3d;

Vertex *pv;

Point2d *p2d;

Point3d *p3d;

那么下面这个指定操作:

pv=&v3d;

需要这样的内部转化:

pv = (Vertex *)(((char*)&v3d)+sizeof(Point3d));

而下面的指定操作:

p2d = &v3d;

p3d = &v3d;

都只需要简单的拷贝其地址就好。如果有两个指针如下:

Vertex3d *pv3d;

Vertex *pv;

那么下面的指定操作:

pv = pv3d;

不能够只是简单地被转换为:

pv = (Vertex *)(((char*)&v3d)+sizeof(Point3d));

因为当pv3d为时,pv将获得sizeof(Point3d)的值,这是错误的!需要加一个条件测试,如下:

pv = pv3d ? (Vertex *)(((char*)&v3d)+sizeof(Point3d)):0;

关于Vertex3d的布局如下:


C++Standard并未要求Vertex3d中的base classes Pointed3d和Vertex有特定的排列顺序。但目前的编译器都根据声明顺序来排列他们。

如果要存取第二个base class中的一个data member,不需要付出额外的成本,因为members的位置在编译时就固定了,因此存取members只是一个简单的offset运算。

4.虚拟继承

多重继承的一个语意上的副作用就是,它必须支持某种形式的“shared subobject继承”。一般的实现方法如下所述:class如果内含一个或多个virtual base class subobject,将被分割成两个部分:一个不变区域和一个共享区域。不变区域中的数据,不管后继如何衍化,总是拥有固定的offset,所以这一部分数据可以直接被存取。至于共享区域,所表现的的就是virtual base class subobject。这一部分的数据,其位置会因为每次的派生操作而有变化,所以它们只可以被间接存取。各家编译器实现技术之间的差异就在于间接存取的方法不同。根据以下程序说明三种主流策略:

class Point2d{

public:

protected:

float _x,_y;

};

class Point3d:public virtual Point2d{

public:

...

protected:

float _z;

};

class Vertex:public virtual Point2d{

public:

...

protected:

Vertex *next;

};

class Vertex3d:public Vertex,public Point3d{

public:

...

protected:

float mumble;

};

一般布局是先安排好derived class的不变部分,然后再建立其共享部分。

存取class共享部分的方法1:cfornt编译器会在每一个derived class object中安插一些指针,每个指针指向一个virtual base class。要存取继承的来的virtual base class members可以通过相关指针间接完成。例如,一下Point3d运算符:

void Point3d::operator+=( const Point3d &rhs ){

_x += rhs._x;

_y += rhs._y;

_z += rhs._z;

}

在cfront策略之下,这个运算符会被内部转换为:

_vbcPoint2d->_x += rhs._vbcPoint2d->x;

_vbcPoint2d->y += rhs._vbcPoint2d->y;

_z += rhs._z;

而一个derived class和一个base class的实例之间的转换,像这样:

Point2d *p2d = pv3d;

在cfront实现模型之下,会变成:

Point2d *p2d = pv3d ? pv3d->_vbcPoint2d : 0;

这个实现模型主要有两个缺点:

1) 每一个对象必须对其每一个virtualbase class 背负一个额外指针。

2) 由于虚拟继承串链的加长,导致间接存取层次的增加,而我们希望有固定的存取时间。

对于第二个问题,一些编译器经由拷贝操作取得所有的nested virtual base class指针,放到derived class object之中。

这个模型下的布局如下所示:

存取class共享部分的方法2: Microsoft编译器引入所谓的virtual base class table。每一个class object如果有一个或多个virtual base classes,就会由编译器安插一个指针,指向virtual base class table。至于真正的virtual base class指针则被放在表格中。

存取class共享部分的方法3: 是在virtual function table中放置virtual base class的offet。Virtual function table可由正值或负值来索引。如果是正值,很显然就是索引到virtual function;如果是负值,则是索引到virtual base class offsets。在这样的策略下,Point3d的operator+=运算符号被转换成一些形式:

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

而Point2d *p2d = pv3d;

会变成:

Point2d *p2d = pv3d ? pv3d + pv3d->_vptr_Point3d[-1] : 0;

这种方法的布局如下图(注意看图中数字表示的偏移量):


经由一个非多态的class object来存取一个继承而来的virtual base class的member,像这样:

Pointorigin;

origin._x;

可以被优化为一个直接存取操作,就好像一个经由对象调用的virtual function调用操作一样,可以在编译时期被决议完成。但如果是用指向对象的指针或引用来存取一个继承而来的virtual base class的member,则如以上分析运作。

3.5对象成员的效率

虚拟继承会降低类数据成员的存取效率。

3.6指向Data Members的指针

已知:

class Point3d{

public:

virtual ~Point3d();

void static show(){

printf("&Point3d::x = %p \n",&Point3d::x);

printf("&Point3d::y = %p \n",&Point3d::y);

printf("&Point3d::z = %p \n",&Point3d::z);

}

protected:

static Point3d origin;

float x,y,z;

};

则&Point3d::z;

上述操作将得到z坐标在class object中的偏移地址。最低限度为x和y的大小总和,然后vptr位置没有限制,故上诉操作的值要不就是8要不就是12。然而编译器为了区分一个“没有指向任何data member”的指针,和一个指向“第一个data member”的指针,会使data member的地址总是加1。也就是说上诉Point3d中的三个数据成员偏移地址要不是1,5,9就是5,9,13。

在如下测试程序中:

int main(){

Point3d::show();

return 0;

}

在VS2008上执行结果如下:


这里之所以显示为0,4,8的原因是vptr放在了class object的最前面,并且VS2008做了特殊处理,使执行结果不加1。但在某些编译器(如BCB3)上执行结果为5,9,D。

在如下操作中:

Point3d origin;

&Point3d::z;

& origin.z;

Point3d::z返回的是float Point3d::* 类型,而origin.z返回的是float* 类型。

但如果z为static data member那么上诉操作都返回的是float*类型。

#include <iostream>

using namespace std;

struct Base1{int val1,val2;};

struct Base2{int val3;};

struct Derived:Base1,Base2{intvald;};

int main(){

printf("%p\n",&Base1::val1);

printf("%p\n",&Base1::val2);

printf("%p\n",&Base2::val3);

printf("%p\n",&Derived::val1);

printf("%p\n",&Derived::val2);

printf("%p\n",&Derived::val3);

printf("%p\n",&Derived::vald);

return 0;

}

以上程序的执行结果如下:


可能是由于VS2008做了特殊处理,导致&Derived::val3的值也为0,但是由于&Derived::vald值为12,表示在vald前面确实有val,val2和val3存在。

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics