《inside the cpp object model》 阶段性阅读总结(3)
第三章 数据的语义
章前阅读
提到两点:
第一节、绑定数据成员编译器系统会添加一些数据成员去支持一些语言功能。Alignment requirements on the data members and data structures as a whole(不知道怎么翻译,大概意思就是数据成员对齐的需要)
主要是全局的对象和类嵌套对象的绑定问题
第二节、数据成员的布局首先是对象绑定,假如在一个内联函数里返回一个变量,如果全局定义了同名变量,并且在该函数的后面也定义了同名变量,那么较早一些的编译器会错误的返回全局变量。早期解决该问题的办法是使用安全的书写方式(把嵌套变量放到类的首部,或把内联函数的函数体写到类外)。现在的编译器会在整个类检查完之后再去绑定内联函数里的变量。其次是内联函数的参数表,参数表(比如typedef来的变量类型)需要在检查到该函数的时候就去绑定变量,早期的编译器里,需要用户使用安全的书写方式,但后来的编译器会在发现类内的指派之后,将之前的指派设为false
非静态的数据成员会按声明的顺序存储在类对象里面,静态的数据成员会直接存储在程序的数据区里而与类对象无关。在类的开始,编译器会插入一些对象去支持c++的对象模型,比如vptr。但是c++标准允许编译器将这些额外的东西存储在任意的地方,甚至于两个对象成员之间。c++标准允许编译器去任意排列不同访问权限区域的对象成员,但是具体的顺序是预编译器相关的,没有同一的标准。不过似乎没有编译器这么做。访问权限的标识符不占用空间。第三节、数据成员的访问
静态数据成员
无论是通过指针还是对象,对静态数据成员的访问最终都会翻译为对该变量实例的直接访问,这也是唯一的完全相等的情况。如果通过函数返回对象来调用静态成员会怎么样呢?cfront会丢弃这个函数而直接调用静态成员。不过后期的c++编译器会将该调用翻译为两步,函数调用和对静态数据成员的访问。将静态数据成员放到代码区会引起发生冲突,如果两个类恰好定义了同名的静态成员。c++使用name-mangling来确保每个静态成员有唯一的名称。实现的方式是和编译器相关的。有两个方面需要注意:
非静态的数据成员算法要产生唯一的名字。这个唯一的名字要很容易被转义回原始的名称。为了方便编译器与用户交流。
第四节、继承和数据成员非静态的数据成员被直接存储在类的对象里,并且只能通过隐式或者现实的类去调用它。作者问了一个问题,通过指针去访问一个成员和通过类去访问一个成员,这两者有没有明确不同?如果是静态成员的话,会完全等价。但是如果是非静态成员的话,有一种情况会不等价:该类是继承自包含一个虚基类的继承链,并且该成员继承自链中的虚基类。因为这种情况下,我们无法在编译时确定指针指向的是哪个对象,需要在运行时确定。
没有多态的继承
直接继承,会把完整的父类对象存储到子对象里(而不是存储去掉对齐所需的花费后拼凑起来的对象)。之所以要存储一个完整的父类对象,是我们可能要通过子类的对象去访问父类的对象,或者我们需要把子类对象赋值父类对象,切割后的子类一定要是一个完整的父类。
带有多态的继承
编译器考虑将vptr放到类的开始或者结束。放到结束可以很好的兼容c语言。放到开始可以高效的支持多态,但是会失去对c的兼容。倒是到底哪个更好一些尚无定论。多重继承
类似单继承,只不过多存储了几个父类对象虚继承
多重继承带来的问题就是,可能导致重复的父类,也就是菱形继承问题。虚继承解决了这个问题。编译器处理的方式为:将一个包含一个以上虚基类的子类拆为两个区域,一个不变的区域,一个共享的区域,不变的区域里存储的是可以直接访问的数据成员,共享区域里存储虚基类的子类。接下来就是访问问题,如何访问这些虚基类呢?cfront的做法是存储每一个虚基类的指针。这个做法带来两个问题:
直接继承的虚基类的数量不同,子类的花费不一样。(空间)随着继承链的增长,这个间接访问的深度也会增加。(时间)对于第一个问题,有两个解决方案
微软的解决办法是引入虚基类表。(作者说,似乎只有微软的编译器是如此实现的)第二个问题是bjarne喜欢的方法,他将虚基类的偏移量(不是地址)存到虚函数表里。
第五节、对象成员的效率
这一节是各种测试,按照书中所说,不感兴趣的话,直接跳入下一节。。。于是我就跳了。。。第六节、指向成员的指针
指针和效率,依然谈到了效率。。先跳过,等我感兴趣了再来看吧。。