Data 语意学
class X { };
class Y : public virtual X { };
class Z : public virtual X { };
class A : public Y, public Z{ };
sizeof X 的结果为1 //翻译者在visual C++ 5.0上的执行结果 1
sizeof Y的结果为8 //4
sizeof Z 的结果为8 //4
sizeof A 的结果为12 //8
一个空class如:
class X{};//sizeof X == 1
事实上并不是空的,它有一个隐含的1byte,是被编译器添加进去的char,使得这个class的两个object得以在内存中配置独一无二的地址:
X a, b:
if(&a == &b)
事实上,Y和Z的大小收到三个因素的影响:
- 语言本身所造成的额外负担 当语言支持virtual base class时,在derived class中,此负担即为某种形式上的指针,它指向virtual base class subobject,或指向一个相关表格(视编译器实现而定)。
- 编译器对于特殊情况所提供的优化处理 Virtual base class X subobject的1bytes大小也会出现在class Y和Z身上,传统上它被放在derived class的固定部分的尾部。
- Alignment的限制 class Y和Z的大小截至目前为5bytes,在大部分的机器上,群聚的结构体大小会受到alignment的限制,使它们更有效率地在内存中被存取。(alignment是将数值调整到某数的整数倍,在32位机器上,通常alignment为4bytes(32位),以使bus的“运输量”达到最高效率)
Empty virtual base class已经成为C++OO设计的一个特有属于,它提供一个virtual interface,没有定义任何数据,某些编译器对此提供特殊处理,一个empty virtual class被视为derived class object最开头的一部分,也就是说没有花费任何额外空间,这就节省空class所谓的1bytes(因为有个成员,就不需要为空class安插一个char),因此Y和Z的大小是4而不是8(VC++就是这一类型的编译器)
class A的大小是什么呢?很明显,某种程度上必须视你所使用的编译器而定。按第一种情况(没有特殊处理Empty virtual base class),我们可能会回答16,毕竟Y和Z都是8,但事实是12.记住,一个virtual base class subobject只会在derived class中存在一份实体,Class A的大小由下列几点决定:
- 被大家共享的唯一一个class X实体,大小为1byte。
- Base class Y的大小,减去“因virtual base class X”而配置的大小,结果是4bytes,Base class Z的算法亦同,加起来是8bytes
- class A自己的大小:0byte
- class A的alignment数量,前述三项总和是9bytes,class A必须调整至4bytes边界,所以要填补3bytes,结果是12bytes。
C++Standard并不强制规定如“base class subobject”的排列次序或“不同存取层级的data member的排列次序”( 如public和private谁先谁后等)这种琐碎细节,它也不规定virtual functions或virtual base classes的实现细节。C++Standard只说:那些细节由各家厂商自定。
1、Data Member的绑定
了解即可,目前已无意义。
2、Data Member的布局
Nonstatic data member在classobject中的排列顺序将和其被声明的顺序一样。其他的情况,如与access sections,vptr等相关的,C++Standard并未做过多要求,vptr有放最后的,也有放最前的。
3、Data Member的存取
已知程序:Point3d origin; origin.x = 0.0;
x的存取成本是什么?答案视x和Point3d的如何声明而定,x可能是static 或nonstatic,Point3d可能是独立类,也可能从其他类继承而来。
Static Data Member
每一个static data member只有一个实体,存放在程序的data segment中
如果有两个classes,都声明了一个static member freeList,会产生命名冲突,编译的解决办法(name-mangling);
- 一种算法,推导出独一无二的名称。
- 万一编译系统(或环境工具)必须和使用者交谈,那些独一无二的名称可以轻易被推导回到原来的名称。
Nonstatic Data Member
直接存放在每一个class object中。每一个nonstatic data member的偏移量在编译时期即可获知,因此,存取效率与C struct member是一样的。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; } 要对nonstatic data member进行操作,编译器需要把class object的起始地址加上data member的偏移量,如: origin._y = 0.0; 那么地址&origin._y等于: &origin + (&Point3d::_y - 1); -1操作原因:由于指向data member的指针其offset总是被加上1,这样编译系统就能区分“一个指向data member的指针,指出class的第一个member”和“一个指向data member的指针,但没有指出任何member”两种情况。
虚拟继承会为“base class subobject”存取class memeber导入一层新的间接层,如:Point3d *pt3d;pt3d->_x = 0.0;其效率在_x是一个struct member,class member,单一继承,多重继承的情况下完全一致。但如果_x是一个virtual base class 的member,存取速度会慢一点。
origin.x = 0.0;pt->x = 0.0;从origin和pt存取有什么重大差异?答案是“当Point3d是一个derived class,而在其继承结构中有一个virtual base class,并且被存取的x是一个从该virtual base class 继承而来的member时,就会有重大差异”。这时候我们不能说pt必然指向哪一种class type(因此我们也不知道编译时期这个member真正的offset位置),所以这个赋值操作必须延迟至执行期。但如果使用origin,就不会有这个问题,其类型无疑是Point3d class。
4、继承与Data Member
在C++继承模型中,派生类成员和基类成员的排列次序,理论上编译器可以自由安排,在大部分编译器中,基类成员总是先出现,但属于virtual base class 的除外。
只要继承不要多态
有一个设计,就是从Point2d派生一个Point3d,于是3d将继承2d数据和操作方法。一般而言,具体继承,相对于虚拟继承并不会增加空间或存取时间上的额外负担。把两个独立不想干的classes凑成一对“type/subtype”,并带有继承关系,会有什么易犯的错误呢?
- 经验不足的人可能会重复设计一些相同操作的函数如operator+=
- 把一个class分解为两层或多层,有可能会为了“表现class体系的抽象化”而膨胀所需空间
C++语言保证“出现在derived class中的base class subobject有其完整原样性”:class C{private:int val;char c1;char c2;char c3;};在一部32为机器中,每一个C object的大小都是8bytes;val 4bytes, c1,c2,c3各1bytes,alignment 1 bytes;如果把C分为三层结构:class C1{private:int val;char bit1;};
class C2 : public C1{private:char bit2;};
class C3 : public C2{private:char bit3;};从设计观点来看,这个结构可能更合理,从效率观点来看,现在C3的大小是16bytes,比原先设计多了一倍。加上多态
而外的负担:
- 导入一个有关的virtual table,用来存放声明的每一个virtual functions的地址,在加上一个或两个slots用以支持runtime type identification
- 在每一个class object中导入一个vptr
- 加强constructor,使它能够为vptr赋初值,让它指向class所对应的virtual table
- 加强destructor,使它能够清除指向class相关virtual table的vptr
多重继承
单一继承提供了一种“自然多态”形式,是关于classed体系中的base type和derived type之间的转换。base class 和derived class的object都是从相同的地址开始的,期间差异只在于derived object比较大。
多重继承的问题主要发生于derived class object和其第二或后继base class objects之间的转换:
对一个多重派生对象,将其地址指定给“最左端(也就是第一个)base class的指针”,情况将和单一继承时相同,因为二者都指向相同的起始地址,至于第二个或后继base class的地址操作,则需要将地址修改过:
//Point3d继承Point2d,Vertex3d多重继承Point3d和Vertex
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;
不能只是简单的地址转换,因为如果pv3d为0,pv将获得sizeof(Point3d)的值,这是错误的,因为,内部需要一个条件测试:
pv = pv3d ? (Vertex*)((char*)pv3d) + sizeof(Point3d) : 0;
C++Standar 并未要求Vertexd中base class Point3d和Vertex有特定的排列次序。
虚拟继承
istream继承iosostream继承ios,iostream多重继承istream和ostream,不论是istream还是ostream都内含一个ios subobject,然而在iostream的对象布局中,我们只需要一份ios subobject就好,解决办法是导入所谓的虚拟继承。(对于更深层次的问题,如固定部分和共享部分数据的存储问题等,暂不记录,有需要可回头重看)
5、对象成员的效率
书中列举几个测试表明,如果编译器将优化开关打开的情况下,C++的封装就不会带来执行期的效率成本(Data 成员的存取),使用inline调用函数也一样。
单一继承也不会影响效率,因为members被连续存储在derived class object中,并且其offset在编译期就已知了。而虚拟继承效率则令人失望。
6、指向Data Members的指针
指向data members的指针,可以用来调查vptr是放在class起始处还是尾端,也可以用来决定class中access sections的次序。class Point3d { public: virtual ~Point3d(); protected: static Point3d origin; float x, y, z; }; 唯一可能因编译器不同而不同的是vptr的位置。 那么存取某个坐标成员的地址,代表什么意思? & Point3d::z; 上述操作将得到z坐标在class object中的偏移量,最低限度其值是x和y的大小总和,然而vptr的位置没有限制。在一部32位机器上,每一个float是4bytes,所以其值要么是8,要么是12; 如果vptr放在对象的尾端,则三个坐标值在对象布局中的offset分别是0,4,8,如果vptr放在对象的起头,则是1,5,9或5,9,13.总是多1,为什么呢? 问题在于,如何区分一个“没有指向任何data member”的指针和一个指向“第一个data member”的指针? float Point3d::*p1 = 0; float Point3d::*p2 = &Point3d::x; //Point3d::*的意思是指向Point3d data member的指针类型 //如何区分 if(p1 == p2) 为了区分p1和p2,每一个真正的member offset都被加上1. 认识指向data member的指针之后,要解释: & Point3d::z; 和 & origin.z; 之间的差异,就非常明确了,一个是它在class 中的offset,一个是真正的class object的data member在内存中的地址。
指向members的指针的效率问题
未优化的情况下,要比直接存取多出一倍不止。(因为指针多了一层间接层)单一继承不影响效率,虚拟继承妨碍了优化的有效性,每一层虚拟继承都导入一个额外层次的间接性。