//整理之,分享之,欢迎指正。for_wind
1、类的实际大小。
每一个class object必须有足够的大小以容纳它所有的nonstatic data members。除了其nonstatic data members的总和大小外,其值的大小受以下三个因素影响:
(1)
语言本身所造成的额外负担:即为了支持virtual而由内部产生的额外负担。当语言支持virtual base class 时,在derived class 中,这个额外的负担体现在某种形式的指针
bptr身上,它或者指向virtual base class subobject,或者指向一个相关表格(表内存放virtual base class subobject的地址,即其偏移量offset);此外还有因虚函数产生的
vptr。
(2)
编译器对特殊情况所提供的优化处理:某些编译器会对empty virtual base class提供特殊支持。
(3)
alignment的限制:将数值填补到某数(如4bytes(32位))的整数倍,以使bus的“运输量”达到最高效率。
备注:
- 一个空的class,其值的大小并不为零。它有一个隐晦的1byte,那是被编译器安插进去的一个char。这使得这个class的两个objects得以在内存中配置独一无二的地址。
- 一个virtual base class subobject只会在derived class中存在一份实体,不管它在继承体系中出现了多少次。
举例:
下图给出class X, Y, Z, A之间的继承关系。
问class X, Y, Z, A的大小分别为多少。
下图给出X,Y,Z的对象布局(编译器不做优化处理)(1,8,8)
下图给出X,Y,Z的对象布局(编译器做了优化处理,不安插一个char)(1,4,4)
上例中的class A的大小又该如何计算,其内存布局又该如何?
(1)被Y、Z共享的
唯一的一个class X实体,大小为1byte;(对empty virtual base class特殊处理的编译器会将(1)中class X实体的1byte 拿掉)
(2)Base class Y的大小,减去“因virtual base class X而配置的大小”,结果是4bytes。同理Base calss Z。共8bytes。
(3)class A自己的大小为:0byte。
(4)class A的alignment数量。
class A的大小,在有特殊优化的编译器上运行结果为8bytes;在无特殊优化的编译器上运行结果则为12bytes。
注意:非空virtual base class两种编译器产生的对象布局一致。
下面给出VS编译器下的情况。
#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( void )
{
cout <<"sizeof X=" << sizeof( X )<<endl ;
cout <<"sizeof Y=" << sizeof( Y )<<endl ;
cout <<"sizeof Z=" << sizeof( Z )<<endl ;
cout <<"sizeof A=" << sizeof( A )<<endl ;
return 0 ;
}
运行结果:
VS2010中
class X的内存布局:
VS2010中
class Y的内存布局:
VS2010中
class Z的内存布局:
VS2010中
classA 的内存布局:
分析:
VS2010编译器中,对每个继承自虚基类的类实例,将增加一个隐藏的
“虚基类表指针”(vbptr)成员变量,从而达到间接计算虚基类位置的目的。该变量指向一个全类共享的偏移量表,表中项目记录了对于该类而言,“虚基类表指针”与虚基类之间的偏移量。
class X的大小为1byte,而class Y,Z的大小为4bytes,含有一个vbptr,即该编译器把vbptr放在前面,对empty virtual base class进行特殊处理,去除了1byte。
而class A的大小的大小为8bytes,具体分析见上。
2、Data member的绑定
对member functions函数本身的分析将延迟直至class声明的右大括号出现才开始,因此在一个inline member function 内的一个data member绑定操作,在整个class 声明完成之后才发生。
然而,这对member function中的argument list并
不为真。Arugment list中的名称会在第一次遇到时被适当地决议resolved。因此最好把“nested type 声明”放在class的起始处,以确保非直觉绑定的正确性。
举例:
#include <iostream>
using namespace std;
typedef int length;
class Point3d
{
public:
//typedef float length;//pos2
void fun1(length val)
{
_val = val;
}
length fun1()
{
return _val;
}
private:
typedef float length;//pos1
length _val;
};
int main(void)
{
Point3d p;
p.fun1(1.2);
cout<<p.fun1()<<endl;
return 0;
}
"nested type 声明”放在位置pos1:第一次遇到函数参数后,决议为global typedef,因此为int
运行结果:
"nested type 声明”放在位置pos2:注释掉pos1那句后
运行结果:
3、Data member的布局
(1)对于
nonstatic data member,把数据直接存放在每一个class object之中,继承而来的nonstatic data member也是如此,不过不强制其排列顺序。
(2)对于
static data member,则把被放在程序的global data segment中,永远只存在一份实体,和个别的class object无关。 template class稍微不同。
(3)access section(private、public、protected等区段)中members的排列
只需符合“较晚出现的members在class object中有较高的地址”。并非一定得连续。(凡处于同一个access section中的数据,必定保证以其声明的次序出现在内存布局当中,然而被放在多个access section的各个数据,排列次序就不一定了)但是当前的编译器都是把一个以上的access sections 连锁在一起,依照声明次序,形成一个连续区块。
(4)对于那些由编译器
内部产生的data members(如vptr)允许自由放在任何位置。当前所有编译器把vptr安插在每一个“内含virtual function的class”的object内(最后,或者最前)。
4、Data member的存取
(1)对于
static data member,
每次程序取用static member,就会被内部转换为对该唯一的extern实体的直接参考操作。存取static members并不需要通过class object。对于继承而来的static member其存取路径也是同样直接。(因为static members只存在唯一的一份实体)
(2)对于
nonstatic data member,
每一个nonstatic data member的偏移量offset,在编译时期即可获知。(派生自单一或多重继承串链也一样)。
而当虚继承时,虚继承将为“经由base class subobject 存取 class members”导入一层新的间接性。
总结:
将对象分割为两部分,一个不变局部和一个共享局部。不变局部中的数据,不管后继如何衍化,总是拥有固定的偏移量,所以这一部分数据可以被直接存取,至于共享局部,所表现的就是虚拟继承的基类子对象,这一部分的数据,其位置会因为每次的派生操作而有变化,所以它们是间接存取。
举例:
Pointed origin, *pt = &origin;
origin.x = 0.0;
pt->x = 0.0;
从origin和pt存取有何差异?
答:当Point3d是一个derived class,而在其继承结构中有一个virtual base class,并且被存取的member是一个从该virtual base class继承而来的member时,有差异。
从pt存取,这时我们不知道pt指向哪一种class type,即无法知道编译时期这个member真正的offset位置,这个存取操作必须延迟至执行期,经由一个额外的间接引导,才能够解决。存取速度比较慢一些。从origin存取,origin的类型是明确的,members的offset位置在编译时期就固定了。
5、Data member与继承
(1)多态与多重继承
多态的弹性带来空间和存取时间上的额外负担:
- 添加virtual table;
- 添加vptr,提供执行期的链接。
- 加强constructor,为vptr设定初值,让它指向class所对于的virtual table;
- 加强destructor,消除指向class virtual table的vptr。
多重继承的问题:
在于derived class objects和其第二或其后继的base class objects之间的转换。
修正:加上或减去介于中间的base class subobjects大小。
(最左端或说第一个base class都指向相同的起始地址,无须修正)
举例1:
class Point2d
{
public:
virtual void fun1(){ };
protected:
float _x, _y;
};
class Point3d : public Point2d
{
public:
virtual void fun1(){ };
protected:
float _z;
};
class Vertex
{
public:
virtual void fun2(){ };
protected:
Vertex *next;
};
class Vertex3d : public Point3d, public Vertex
{
public:
virtual void fun1(){ };
virtual void fun2(){ };
void fun3(){ };
protected:
float mumble;
};
下图是VS2010中
class Vertex3d的内存布局(多重继承):
对比上下两图:
除了vfptr的位置(在前或在后)不同,其他内容是一致的。对于单一继承和多重继承,VS2010编译器采用直接复制模型,将base class subobject的data members被直接放在derived class object中。
此外也注意到
-16。
下图是可能的数据分布(多重继承):
(2)虚继承
举例2:
class Point2d
{
public:
virtual void fun1(){ };
protected:
float _x, _y;
};
class Point3d : public virtual Point2d
{
public:
virtual void fun1(){ };
protected:
float _z;
};
class Vertex : public virtual Point2d
{
public:
virtual void fun2(){ };
protected:
Vertex *next;
};
class Vertex3d : public Point3d, public Vertex
{
public:
virtual void fun1(){ };
virtual void fun2(){ };
void fun3(){ };
protected:
float mumble;
};
下图是VS2010中
class Point2d的内存布局(虚继承):
- 观察class Point2d的内存布局,
Point2d::$vftable@:
| &Point2d_meta //第一次声明
| 0 //偏移量
0 | &Point2d::fun1 //函数名
下图是VS2010中
class Point3d的内存布局(虚继承):
- 观察class Point2d和Point3d的内存布局,
可知,VS2010编译器中对于虚继承,是在
直接复制模型的基础上,选择了virtual base class table模型:即增加了一个vbptr。
观察到Point3d中
vbptr指向的virtual base class table如下:
Point3d::$vbtable@:
0 | 0
1 | 8 (Point3dd(Point3d+0)Point2d)
偏移量
0是相对整个derived class而言,而8是该vbptr相对于virtual base Point2d的位置。
此外注意到,直接复制(继承)的
vfptr应该进行修正。
Point3d::$vftable@:偏移量为
-8,即指向base Point2d的vfptr位置。
下图是VS2010中
class Vertex的内存布局(虚继承):
- 观察class Point2d和Vertex的内存布局,
可知,VS2010编译器中
对于虚继承,
直接复制后,增加了一个vbptr。
观察到Vertex中
vbptr指向的virtual base class table如下:
Vertex::$vbtable@:
0 | -4
1 | 8 (Vertexd(Vertex+4)Point2d)
偏移量
-4是相对整个derived class而言,而8是该vbptr相对于virtual base Point2d的位置。
对于新增的虚函数:添加新的
vfptr(注:放在vbptr之前)
Vertex::$vftable@Vertex@:
| &Vertex_meta //第一次声明
| 0
0 | &Vertex::fun2
此外注意到,直接复制(继承)
vfptr应该进行修正。
Vertex::$
vftable@Point2d@:偏移量为
-12,即指向base Point2d的vfptr位置。
下图是VS2010中
class Vertex3d的内存布局(虚继承):
- 观察class Point3d、Vertex和Vertex3d的内存布局,
对于虚继承,是在
直接复制模型的基础上,virtural base class Point2d只
复制一份。
注意到,
Vertex3d::$vftable@Vertex@:
| &Vertex3d_meta
| 0 //Vertex3d::$vftable@Vertex@:偏移量为0,即指向base Vertex的vfptr位置。
0 | &Vertex3d::fun2
Vertex3d::$vftable@:
| -24 //Vertex3d::$vftable@::偏移量为-24,即指向base Point2d的vfptr位置。
0 | &Vertex3d::fun1
直接复制(继承)的
vfptr应该进行修正。
注意到,
Vertex3d::$vbtable@Point3d@:
0 | 0
1 | 12 (Vertex3dd(Point3d+0)Point2d)
Vertex3d::$vbtable@Vertex@:
0 | -4 // Vertex3d::$vbtable@Vertex,偏移量-4是相对整个derived class而言,而20是该vbptr相对于virtual base Point2d的位置。
1 | 20 (Vertex3dd(Vertex+4)Point2d)
6.指向Data Members的指针
如何区分一个“没有指向任何data member”的指针和一个指向“第一个data member”的指针?
如
float Point3d::*p1 = NULL;
float Point3d::*p2 = &pointed::x;
每一个真正的member的offset值都被加上1。(为此,在真正使用该值去指出一个member之前,需先减掉1。)
如何通过origin.z计算origin的起始地址?
取一个nonstatic data member的地址,将得到它在class中的offset;
取一个绑定于真正class object身上的data member的地址,将会得到该member在内存中的真正地址。
因此 , &origin.z所得结果 - z的偏移量(相对应origin的起始地址)
+ 1 = origin的起始地址
参考资料及推荐资料: