目录
对于下述类和继承
class X{};
class Y:public virtual X{};
class Z:public virtual X{};
class A:public Y,public Z{};
计算上述每个类所占内存空间如下:
对于上述每个类的大小的讨论:一个空的class,如x,sizeof(X)==1。事实上虽然X中无任何数据,但他有一个隐晦的1byte,那是被编译器安插进去的一个char,这使得这个类的两个对象得以在内存中配置独一无二的地址。
Y和Z的大小受到三个因素的影响:
- 语言本身所造成的额外负担:当语言支持虚基类时,会造成一些额外负担。
- 编译器对于特殊情况所提供的优化处理:虚基类的X子对象的1bytes大小也会出现在类Y和Z身上。
- Alignment(边界调整,调整至某些bytes的倍数,结果视不同机器而定)的限制:大部分机器上,群聚的结构体大小会受到alignmeny的限制,使他们能够更有效率地在内存中被存取。
x和y和z的对象布局
接下来计算A的大小:正常下,A应该是16bytes,但确是12bytes
一个虚基类子对象只会在派生类中存在一份实体,不管他在类的继承体系中出现了多少此,类A的大小由如下几点决定
- 被大家共享的唯一一个X实体,大小为1byte。
- Y的大小,减去因虚基类X而配置的大小,结果是4bytes,Z同理,加起来后为8bytes
- class A自己大小:0byte
- A的alignment数量:前三项和为9bytes,需要调整至4bytes边界,所以需补充3bytes,结果为12bytes。
3.1 数据成员的布局
下述数据成员
class Point3d
{
public:
//
private:
float x;
static List<Point3d*> *freeList;
float y;
static const int chunkSize=250;
float z;
};
非静态成员数据在类对象中的排列顺序和其被声明的顺序一样,任何中间介入的静态成员数据都不会被放进对象布局之中,上述Point3d对象是由三个float组成的,次序是x,y,z。静态成员数据存放在程序的data segment中,和个别的类对象无关。
C++规定,在同一个access section(即private,public,protected等区段)中,成员的排列只需符合”较晚出现的成员在类对象中有较高地址“这一条件即可,不一定需要连续排列。
编译器还会合成一些内部使用的数据成员,以支持整个整个对象模型,vptr就是一个例子,传统上放在所有明确声明的成员的最后,不过也有编译器将其放在类对象的最前端
C++标准也允许编译器将多个access sections之中的数据成员自由排列,不必在乎他们出现在类声明中的次序。例如下述声明:
class Point3d
{
public:
//
private:
float x;
static List<Point3d*> *freeList;
private:
float y;
static const int chunkSize=250;
private:
float z;
};
其类对象的大小和组成和前面声明的相同,但是成员排序次序由编译器决定。
3.2 数据成员的存取
3.2.1 静态数据成员
静态数据成员,被编译器提出与类之外,每一个成员的存取许可,以及与类的关联,并不会
导致任何空间上或执行时间上的额外负担,以及与类的关联,并不会导致任何空间上或执行时间上的额外负担。
每一个静态数据成员只有一个实体,存放在程序的data segment之中,每次程序参阅静态成员,就会被内部转化为对该唯一的extern实体的直接参考操作。例如,Point3d origin
orgin.chunkSize==250;
经由成员选择运算符(即.运算符)对每一个静态成员进行存取,只是语法上的一种便宜 行事而已,成员并不存在类对象之中,因此存取静态成员并不需要通过类对象。
就算chunkSize是从继承关系中得到的,其存取路径还是如此唯一。
3.2.2 非静态数据成员
非静态数据成员直接存放在每一个类对象之中,除非经由明确的explict或暗喻的implicit的类对象,没有办法直接存取他们。例如下述代码
Point3d Point3d::translate(const Point3d &pt)
{
x+=pt.x;
y+=pt.y;
z+=pt.z;
}
表面上看到对于x,y,z是直接存取,事实上是经由一个implicit class object(由this指针表达)完成,事实上这个函数参数是:
Point3d Point3d::translate(Point3d * const this const Point3d &pt)
{
this->x+=pt.x;
this->y+=pt.y;
this->z+=pt.z;
}
对非静态数据成员进行存取操作,编译器需要吧类对象的起始地址加上数据成员的偏移量。例:
origin._y=0.0;
那么地址&orgin._y将等于
origin+(&Point3d::_y-1)
每一个非静态数据成员的偏移量在编译期间即可获知,甚至如果成员属于一个基类子对象也是一样,因此,存取一个非静态数据成员,其效率和存取一个C struct menmber或一个非派生类的成员是一样的。
例
Point3d origin,*pt=&orgin;
origin.x=0.0
pt->x=0.0;
从origin存取和从pt存取,存在差异。当Point3d是一个从派生类,而在其继承结构中有一个虚基类,并且被存取的成员(即x)是一个从该虚基类继承而来的成员时,就会存在重大差异。这时候无法说pt必然指向哪一种类类型(因为不知道编译器期时这个member真正的偏移位置),所以这个存取操作必须延迟到执行期,经由一个额外的间接引导才能解决,但如果使用origin,就会不存在上述问题,其类型无疑是Point3d类,而即使他继承自虚基类,成员的偏移位置在编译期就固定了,编译器便可以经由origin解决对x的存取。
3.3 继承与数据成员
在C++继承模型中,派生类的东西是其自身成员加上基类成员的综合。至于继承类成员和基类成员的排序次序没有强制规定,大部分编译器中,基类成员总是先出现,但属于虚基类的除外。
3.3.1 只要继承不要多态
例如,有下述继承关系类
其对象布局如下:
我们声明一组指针
Concrete2 *pc2;
Concrete1 *pc1_1,*pc1_2;
若执行下述操作:*pc1_2=*pc1_1,将会逐个进行成员复制。
然而,如果c++把派生类成员和Concrete1的子对象绑定在一起,去除填补空间,则下述操作则会存在问题:pc_1=pc2。图解如下
3.4 指向数据成员的指针
考虑下述类
class Point3d
{
public:
virtual ~Point3d();
protected:
static Point3d origin;
float x,y,z;
};
每一个point3d含有x,y,z和一个vptr,静态数据成员origin被放在类对象之外。根据编译器的不同,vptr所在位置不同,可能为起始处或尾端。
取某个成员地址: &Point3d::z,将得到z在类对象中的偏移量,最低限度其值为x和y的大小总和,因为c++要求同一个access level的成员排序次序和声明次序相同。
在一个32位机器上,每一个float是4bytes,所以,如果vptr在尾端,则三个坐标值的偏移量分别为0,4,8,如果vptr在对象的头部,则三个坐标值的偏移量分别为4,8,12.然而最终结果却总是多1,为1,5,9或5,9,13.原因如下。
float Point3d::*p1=0;
float Point3d::*p2=&Point3d::x;
//Point3d::*代表指向Point3d的数据成员的指针类型
为了区分p1和p2,每一个真正指向成员的偏移量都被加上1,因此,在真正使用该值以指出每一个成员之前,需要先减掉1.