深度探索C++对象模型笔记(三)

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的大小收到三个因素的影响:

  1. 语言本身所造成的额外负担 当语言支持virtual base class时,在derived class中,此负担即为某种形式上的指针,它指向virtual base class subobject,或指向一个相关表格(视编译器实现而定)。
  2. 编译器对于特殊情况所提供的优化处理 Virtual base class X subobject的1bytes大小也会出现在class Y和Z身上,传统上它被放在derived class的固定部分的尾部。
  3. 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);

  1. 一种算法,推导出独一无二的名称。
  2. 万一编译系统(或环境工具)必须和使用者交谈,那些独一无二的名称可以轻易被推导回到原来的名称。

Nonstatic Data Member

直接存放在每一个class object中。
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”两种情况。
每一个nonstatic data member的偏移量在编译时期即可获知,因此,存取效率与C struct 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的指针的效率问题

未优化的情况下,要比直接存取多出一倍不止。(因为指针多了一层间接层)
单一继承不影响效率,虚拟继承妨碍了优化的有效性,每一层虚拟继承都导入一个额外层次的间接性。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值