Inside C++ Object Model阅读笔记:Chapter 3 数据语义学

来源:互联网 发布:网络系统检测报告 编辑:程序博客网 时间:2024/05/08 14:09
Chapter 3 数据语义学

对于一个空类,编译器将插入一个char成员,因而类生成的对象在内存中的位置将不同。
对于虚基类,需要一个指针指向基类位置或者相应的对象内存分布表。对纯虚接口类,因为在OO中起到重要作用,所以有编译器会支持不消耗内存的纯虚基类。虚基类在子类中只出现一次,在子类的对象中,虚基类的对象只有一个实例。
编译器可能因为内存对齐原因来调节类的大小。
每个对象的大小都应当等于它所包含所有非静态数据成员的大小。它们可能会因为两种原因变得更大:
1、为了实现语言特性(例如多态性)编译器提供了更多的成员。
2、系统对齐要求更多的字节。

3.1 数据成员绑定
对数据成员的绑定必须放在类的最前方。
例如:
typedef double length;
class T {
    public:
        void dosth(length a) {...};
   private:
        typedef int length;
};
将是无效的,因为编译dosth的时候length是double而之后类内又重新定义了length的类型。

3.2 数据成员布局
静态数据成员的位置储存在程序的数据段(data segment)中。
标准还要求在一样的访问层次中,放在后面的成员有较高的地址。这也表明成员不需要连续的排列,在成员之间还会有为了满足对齐而设置的数据。
另外还可能有vptr或者其他的编译器合成的元素。vptr现在一般放在第一个位置。
标准允许多重访问层次放置位置的交错。例如:
private: float x;float x1;
private: float y;
public: float z;
x,y,z的位置可能并不是按照顺序放置的;但是x1肯定在x之后。
一般来说编译器会把相同访问层次的成员放在一起。但是这样做不会影响类的大小。

3.3 数据成员访问
静态数据成员位于对象外部,处在数据段中,是全局变量。访问的时候和全局变量无异。
如果访问函数返回的对象中的全局变量,则必须先计算出函数的返回值,再访问相应的静态变量,虽然可以通过函数类型推断出静态变量的地址。取这个静态成员的地址,等于取相应成员类型的地址,而与对象类型无关。
对于非静态数据成员,它们被存放在对象中,不能直接访问。在类内的成员函数中,对本对象的访问也是通过this指针进行的。具体访问的细节如下:首先确定对象的首地址,然后推算出数据成员的偏移。这种偏移是在编译时期就计算好的。然而虚继承将导入进一步的重定向。

3.4 继承和数据成员
如果不使用多态性机制,则继承类的结构将是紧密堆积的。通过继承来将两个独立的类结合,可能犯两种错误:
1、不在适当位置使用内联会导致更多的调用。
2、内存对齐可能会导致过多的资源的消耗。因为内存对齐时填充的字节在派生类中不能使用,否则对成员的复制将产生问题。
使用虚函数来增加多态性,将带来一系列时空惩罚:
1、使用虚函数将增大vtbl。
2、使用虚函数将导致必须通过vptr来访问真正数据。
3、消耗构造对象的时间。
4、消耗析构对象的时间(对象析构:先析构派生类再析构基类)
vptr的位置:
1、放在最后:保存了类似C struct的结构,这样可以允许以C的形式访问对象。
2、放在最先:可以更有效的支持多重继承的对象成员函数访问。
单继承时直接将派生类数据放在基类之后,因而可以直接令基类指针指向派生类对象(基类地址偏移不变)。然而多重继承可能导致“不自然”的格式,因而在用第二个,第三个……基类的指针指向派生类成员的时候,需要进行转换。这个转换将指针地址跳过了前面基类的数据,直接指向派生类对象中相应数据的位置。同时,在这时需要对派生类指针加以判断。如果派生类指针为NULL则返回NULL,否则返回叠加了内存布局靠前位置的其他基类数据的位移。对于每个基类在派生类中的相应位置,没有一个明确的规定,一般按照设计时的顺序排列;一个可能的优化是将含有虚函数的类提前以节省一个虚指针的位置。访问的时候,由于类内的相对偏移已经确定,所以不存在时间开销。
虚继承下,需要支持多个类共有的一个基类。一般的实现如下:将虚继承的对象分成两个部分,一个是独立部分,一个是共有基类部分。独立部分的数据仍然保持一个固定的偏移,无论对应的类在对象的什么地方;共有基类部分数据的偏移是浮动的,需要通过间接途径访问。编译时首先生成独立部分,再生成共有部分。
一种解决方法(cfront)是:共有部分通过一个链接指针访问。缺点为:
1、对于每一个虚基类成员需要携带一个额外的指针。
2、继承链增长的时候,需要更多的指针,这样访问将随着继承而消耗更多时间。
为了解决第二个问题,有一些编译器将继承的链接指针都提升到最上层的派生类中。
为了解决第一个问题,有两种途径。M$的编译器引入了虚基类表,每个包含虚继承的对象都与一个虚基类表相连,虚基类指针在表中储存。另外一个解决方法是,在虚函数表中放置虚基类的偏移量。
最有效的解决方法是:只使用无数据成员的纯虚类。

3.5 对象成员效率
如果不使用虚拟继承,则访问堆栈上的对象将和访问普通变量有同样效率(都是计算偏移量,取值),但是一旦使用了虚拟继承,将会受到抽象的惩罚。这种惩罚在多重虚拟继承下,是相当严厉的。

3.6 指向数据成员的指针
指向数据成员的指针是一种相当有用的语言特性。(附注:这些指针代表对象成员相对于对象起始点的偏移量)值得主意的是,取得的指针的值是实际偏移量+1。这是为了防止无法区分空指针和指向第一个成员的指针。在使用指针的时候,编译器会自动-1。
&Object::z代表的是针对所有对象的起始点的偏移量,而&object.z代表的是object对象中的z成员的物理地址位置。二者的typeid也是不同的。
(附注:使用方法为:
class Object;
Object object;
type Object::*pmember=&Object::member;
object.*pmember...
在多重继承下,派生类中的基类成员较为复杂,传递的时候编译器会自动将基类在派生类中的位置计算在内。
使用这种类型的指针,在没有虚拟继承的情况下不存在抽象惩罚。
原创粉丝点击