首页 > 技术文章 > 深度探索C++对象模型 第三章 Data 语意学

purejade 2014-07-23 11:14 原文

一个有趣的问题:下列 类 sizeof大小

class X{}    //1 

class Y:public virtual X{} //4 or 8

class Z:public virtual X{} // 4 or 8

class A:public Y,public Z{} // 8 or 12

主要原因:为了保持每一个类生成对象在内存中的唯一性,编译器必须要给空类生成一个char来维持object的唯一性;

而virtual继承中,仅保持了base class的指针,有些编译器会继承base的一个char数据,有些编译器会舍去。如果保持,则包含数据,且为5bytes,需要保持alignment,则为8bytes。Class A,同时继承Y和Z。// Visual C++和G++采取empty virtual base class被视为derived class object最开头的一部分,则derived class不需要耗费1char 来维持object的独一性。

G++ 上结果为 1,8,8,16 (由于sizeof(int *) = 8) 

sizeof(Y) = sizeof(pointer in machine)

class W{char b;} sizeof(W) = 1;W w ; sizeof(w) = 1; 表示实际占用的大小,(针对alignment编译器已经做好了优化,不用考虑)

C++对象模型尽量以空间优化和存取速度的考虑来实现nonstatic data members。数据直接存放在每一个class object中。

class object 大小:

1) 实际的数据对象 non-static data member

2) 编译器自动加上的额外data member,用于支持语言特性(主要是virtual)

3) 因为alignment的需要(这个目前来说由底层实现,一般不用考虑,sizeof得到的是实际大小)

3.1 Data Member 的绑定

有意思的一章节,虽然了Class数据绑定的发展

extern float x;

typedef int type;

class A { public : float X(type val) const { return x;} private: float x;typedef float type;} 

//抛出一个问题,函数X返回值是全局的x还是class内部的x?

当然,现在的C++模型肯定是内部的;而之前不一定,从前向后解析,并进行绑定;所以x被绑定到了全局。 

现在进行了改进:只有当把class扫描完之后才对函数本体进行解析。并且遇到新的类型,就标记前一个标记为非法(如float x覆盖int x),用正确的解析。但对于参数,但对于参数仍可能绑定到全局变量(如type val,仍绑定到全局)。

因此我们仍需要采用防御性程序风格: 保证变量声明在定义之前,来确保非直觉绑定的正确性。

1) 总是把声明放到class的最开头

2)总是把inline function放在class体外部;

3.2 Data Member 的布局(Data Member Layout)

由编译器确定,一般规则相同的access section的数据放在同一个区域,也可以多个section放在同一个区域。

3.3 数据的存取

抛出一个问题:origin.x和pt->x 访问有什么区别? origin.x 主要是编译期间静态绑定,根据x的offset偏移量进行访问;而pt可能会不确定类型,需要运行时确定;

主要区别静态数据和非静态数据,静态数据保存在data segment段,可以被类直接访问,所有继承的类也都共享该静态对象,在内存中是唯一性;

为了避免多个静态变量的命名冲突,可以采用name-mangling才进行映射,保证唯一性,并可以反推回来。

非静态数据位只有object才可以访问,默认访问时需要有this(implicit class object)

有一个问题:本文中指出一个指向data member的指针,用以指出class的第一个member,和一个指向data member的指针,两者之间相差一个byte,类似于空class一样。但我在G++上测试,两者的基地址保持一致,并无1byte的相差;

包含virtual function的布局:有的vptr放在数据尾部,保持和struct兼容性;但目前普遍放在class的头部,便于访问virtual function。

3.4 继承与data member

单一继承,多重继承(不包含虚拟继承):按照继承class声明的顺序继承数据,当然继承顺序主要由编译器决定;

书中提到:多重继承中可能由于alignment导致多重继承之后class的size变大。但似乎在G++中,能够有效的优化,没有带来alignment的开销。(经测试)

主要难点是virtual inheritance,虚拟继承主要用来保持数据的唯一性,但也带来了复杂性和效率问题。不同的编译器有不同的处理方式。这里提到两种方法:

1)子类保存对虚拟继承父类的指针,随着虚拟继承嵌套层数的增多,会导致间接访问时间开销过大,需要多层间接访问;

2)为了避免上述情况,保持访问的一致性,将virtual function entries 和virtual base class offset放在一起;可以将虚拟继承的父类对象拷贝到自己的对象空间,同时保持对该空间的指针;

如果同时继承来自多个虚拟class,则仅保留一份对原始父类; (指向父类的指针放在class空间的开头) 【补图】

3.5 和 3.6 主要谈到效率问题和指向members指针

作者测试的环境和现在的不太一样,下一步在探究。

推荐阅读