您的位置:首页 > 编程语言 > C语言/C++

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

2014-01-03 19:00 447 查看

第三章 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;
};

Point3d origin, *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”两种情况。如:
float Point3d::*p1 =0;//
这个就是“没有指向任何Data member”的指针
float Point3d::*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存在。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: