第三章 - Data 语意学
概述
//sizeof X == 1
class X{}
2
即使一个类什么内容都没有,它有隐藏的1byte大小,是被编译器安插进去的一个char, 这使得这一class的两个objects得以在内存中配置独一无二的地址。
Y和Z的大小受到三个因素的影响:
- 语言本身所造成的额外负担。支持虚基类,需要引入vptr。
- 编译器对于特殊情况所提供的优化处理。Virtual base class X subobject的1 bytes大小也出现在 class Y 和 Z 身上。
- Alignment 的限制。字节对齐,Y和Z目前是5 bytes,需要填补3 bytes,最终结果是8 bytes。
对象布局如下:
Empty virtual base class已经成为C++OO设计的一个特有术语了,它提供一个virtual interface,没有定义任何数据。某些新近的编译器对此提供了特殊处理。在这个策略之下,一个empty virtual base class被视为derived class object最开头的一部分,也就是说它并没有花费任何的额外空间。
如果Virtual base class X中放置一个以上的data member,两种编译器(有特殊处理 和 没有特殊处理)就会产生出完全相同的对象布局。
C++对象模型尽量以空间优化和存取速度优化的考虑来表现non static data members,并且保持和c语言struct数据配置的兼容性,它把数据直接存放在每 一个class object之中。对于继承而来的non static datamembers(不管是virtual或non virtual base class)也是如此。不过并没有强制定义其间的排列顺序。
Data Member的绑定(The Binding of a Date Member)
抛砖引玉:
extern float x;
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;
};
2
3
4
5
6
7
8
9
10
11
12
Point3d::X() 返回的是class内部的x,还是外部extern的那个x。现在指向class内部的x,但是早期C++编译器是指向外部extern的那个x。从而引出早期C++的两种防御性程序设计风格。
- 把所有的data members放在class声明起头处,以确保正确的绑定。
class Point3d
{
float x, y, z;
public:
float X() const { return x; }
};
2
3
4
5
6
7
- 把所有的 inline functions 不管大小放在class声明之外。
inline float Point3d::X() const { return x; }
备注:类成员函数,在类体内部(类定义头文件中)定义的函数默认就是内联函数。
一个inline函数实体,在整个class声明未被完全看见之前,是不会评估求值的。
绑定:
上述这种语言状况,仍然需要某种防御性程序风格:请始终把“nested type声明”放在class的起始处,在上述例子中,如果把length的nested typedf定义于“在class中被参考”之前,就可以确保非直觉绑定的正确性。
Data Member 的布局(Data Member Layout)
目前各家编译器都是把一个以上的access sections连锁在一起,依照声明的顺序,成为一个连续区块,Access sections的多寡并不会招来额外负担。例如在一个section中声明8个members,或是在8个sections中总共声明8个members,得到的object大小是一样的。
Data Member 的存取
Static Data Members
Static Data Members 并不在class object之中,因此存取Static Data Members并不需要通过class object。
如果多个class 有同样的Static Data Members,可以采用名字修饰(Name Mangling),以获得独一无二的程序识别代码。
Nonstatic Data Members
每一个nonstatic data member的偏移量(offset)在编译时期即可获知,甚至如果member属于一个base class subobJect(派生自单一或多重继承串链)也是一样。因此,存取一个nonstatic,其效率和存取一个c struct member或一个nonderived class的member是一样的。
Point3d *pt;
Point3d origin;
//...
origin.x = 0.0;
pt->x = 0.0;
2
3
4
5
执行效率在是一个struct member、一个class member、单一继承、多重继承的情况下都完全相同。但如果是一个virtual base class的member,存取速度会比较慢一点。
如果使用origin就不会有这些问题,其类型无疑是3Point3d,而即使它继承自virtual base class,members的offset位置也在编译时期就固定了,一个积极进取的编译器甚至可以静态地经由origin就解决掉对x的存取。
“继承” 与 Data Member
在大部分编译器上头,base class members总是先出现,但属于virtual base class的除外。
只需要继承不要多态(Inheritance without Polymorphism)
Point3d object的初始化操作或加法操作,将需要部分的Point2d object和部分的Point3d object作为成本。一般而言,选择某些函数做成inline函数,是设计class时的一个重要课题。
把一个class分解为两层或更多层,有可能会为了表现class体系之抽象化而膨胀所需的空间
以Concrete为例:
其对象布局如下:
加上多态(Adding Polymorphism)
如果处理坐标点时,不考虑是Point2d 或 Point3d 实例,则需要在继承关系中提供一个 virtual function接口:
Point3d 的新声明:
void operator+=(const Point2d& rhs)
{
Point2d::operator+=(rhs);
_z += rhs.z();
}
2
3
4
5
这样就可以实现 Point2d 和 Point3d 的加法。
把vptr放在class object的哪里比较好:
早期时将vptr放在class object的尾端,随着开始之初虚拟继承以及抽象基类等,某些编译器开始将vptr放在class object的头部。