爱技术 & 爱分享
爱蛋蛋 & 爱生活

C++对象模型演变

本篇文章来讲述一下C++对象模型发展中可能得以应用的几种模型。

对象模型

演化史

已知下面这个Class Point声明:

class Point
{
public:
    Point(float xval);
    virtual ~Point();

    float x() const;
    static int PointCount();

protected:
    virtual ostream& print(ostream& os) const;

    float _x;
    static int _point_count;
};
简单模型(A Simple Object Model)

简单对象模型:一个C++对象存储了所有指向成员的指针,而成员本身不存储在对象中。
也就是说不论数据成员还是成员函数,也不论这个是普通成员函数还是虚函数,它们都存储 在对象本身之外,同时对象保存指向它们的指针。
简单模型

在这个模型中,member 并不放在 object 中,只有”指向 member 的指向“才放在 object 之内,这么做可以避免” members 有不同的类型,因而需要不同的存储空间“所招致的问题。
object中 member 是以 slot 的索引值来寻址,本例中 _x 的索引值是6,_point_count 是7.一个 class object 的大小很容易计算出来,”指针代销,乘以 class 中所声明的 members 数目“便是。

虽然这个模型并没有被用于实际产品,不过关于索引或 slot 个数的概念倒是被应用到C++的“指向成员的指针”观念当中。

表格驱动对象模型(A Table-driven Object Model)

为了对所有 class 的所有 object 都有一致的表达方式,另一种对象模型是把所有与 members 相关的信息抽出来,放在一个 data member table 和一个 member function table 之中。class object 本身含义指向这2个表格的指针。member function table 是一系列的 slots,每个 slot 支持一个 member function,data member table 则直接含有 data 本身。

表格模型

虽然这个模型也没有实际应用于真正的 C++ 编译器身上,但 member function table 这个观念却成为支持 virtual function 的一个有效方案。

C++对象模型(The C++ Object Model)

在 Stroustrup 当初设计(目前大多数厂商采用的方式)的对象模型中,Nonstatic data members 是被配置于每一个 class object 之内,static data members 则被存放在 class object 之外,位于程序的静态存储区,按是否初始化又有不同。

Static和nonstatic function members 也被放在个别的 class object之外,即所有非虚函数都不会跟对象绑定的,不过非静态函数第一个参数默认为this指针。

Virtual function 则是以以下两个步骤支持:

  • 每一个 class 产生出一堆指向 virtual function 的指针,放在一个此类所共有的表格中,这个表格被称之为 virtual table (vtbl)
  • 每一个class object(含有虚函数)被安插一个指针,指向相关的 virtual table。通常这个指针被称之为 vptr。vptr 的设定和重置,都是由每一个 class 的 constructor ,destructtor 和 copy assignment 运算符自动完成的。每一个 class 所关联的 typ_info object (用以支持 runtime type identification,RTTI)也经由 virtual table被指出来 ,通常放在表格的第一个slot(条目)。

C++对象模型

在x86选项的VS2017环境中,内存布局如下:

class Point size(8):
    +---
 0  | {vfptr}
 4  | _x
    +---

Point::$vftable@:
    | &Point_meta
    |  0
 0  | &Point::{dtor}
 1  | &Point::print

Point::{dtor} this adjustor: 0
Point::print this adjustor: 0
Point::__delDtor this adjustor: 0
Point::__vecDelDtor this adjustor: 0

这个模型主要优点是在于它的空间和存取时间的效率,主要缺点是:如果应用程序代码本身未曾改变,但所用到的class object的nonstatic data member有所修改(可能增删改)。那么那些应用程序代码同样得重新编译。关于这一点,前面的双表格模型提供了较大的弹性,因为他多提供了一层间接性,不过它也因此付出空间和执行效率两方面的代价。

在虚继承的情况下,base class 不管在继承链中被派生(drived)多少次,永远只会存在一个实例(称之为 subobject)。例如早期的iostream 中就只有 virtual ios base class 的一个实例。图 ↓
菱形继承

一个 drived class 如何在其内部塑造 base class 的实例呢? 如果是前面讲过的简单模型的话,那很简单,每一个base class 都被 drived class object 内的一个slot 指出,该 slot 内存放的是base class subobject 的地址。

这个方案的主要缺点是,因为间接性而导致空间和存取时间上的额外负担,优点则是class object 的大小不会因为base class 的改变而受到影响

如果是table drived 模型的话。这里所说的base class table 被产生出来时,表格中的每一个slot 内含一个相关的 base class 地址,这很像 virtual table 内含每一个virtual function 的地址一样。每一个 class object 内含一个bptr(potinter to base class),它会被初始化,指向其 base class table。这种策略的主要缺点是由于间接性而导致导致的空间和存取时间上的额外负担,优点则是在每一个class object 中对于继承都有一致的表现方式;每一个class object都应该在某个固定的位置放置一个 base class 指针,与base class的大小或个数无关。第二个优点是,无需改变class object 本身,就可以更改,放大或缩小 base class table。

还是以iostream为例来演示此种模型:图↓
表格驱动的虚继承体系

无论是简单模型还是表格驱动的模型,“间接性”的级数将因为继承的深度而增加。
当然咯,也可以在drived class 内复制一个指针,指向继承体系链中的每一个 base class,这样的话倒是可以获得一个固定的存取时间,不过存那么多指针,空间开销自然就上来了。

C++最初采用的继承模型是不运用任何间接性,base class subobject 的 data member 被直接放置在drived class object 中。 这提供了对base class members最紧凑而且最有效率的存取。
大家肯定也想到了,这么做呢,缺点肯定是有滴。那就是:base class members 的任何改变,包括增加,移除或改变类型等等,都使得所有用到 “此 base class 或其 drived class 的 object”者,必须重新编译。

自C++ 2.0 起才导入的virtual base class ,需要一些间接的base class 表现方式。Virtual table 的原始模型是在class object中为每一个有关联的 virtual base class加上一个指针。其他演化出来的模型则要不是导入一个 virtual base class table,就是扩充原来已经存在的,以便维护每一个 virtual base class 的位置(此部分之后会写文章详细介绍)。

赞(0) 传送门
本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。编程日志 » C++对象模型演变
分享到: 更多 (0)

游戏 && 后端

传送门传送门