《深度探索C++对象模型》:Data member的布局

来源:互联网 发布:淘宝客服转接设置 编辑:程序博客网 时间:2024/06/05 05:44

Data member的布局

先来看一个类,如下:

class Point3d {public:    // ...private:    float x;    static list<Point3d*>* freeList;    float y;    static const int chunkSize = 0;    float z;};
        在上一篇文章的结尾部分,我们提了一下data member的布局,根据这些知识,我们可以知道sizeof(Point3d)为12 bytes。一如之前的风格,我们来看一下class Point3d的对象模型,如下:


        C++ Standard只要求在同一个access section(也就是private、public、proctected等区段),data members的排列只需要符合“较晚出现的members在class object中有较高的地址”,也就是说各个member并不一定是连续排列的,正如class Point3d一样,有可能被其他东西介入。C++ Standard对布局持放任的态度,也就是说你你将class Point3d写成下面这个样子,其对象布局也如上图(当然也看编译器咯,但一般都是相同的),即access sections的多少并不会招致额外的负担。

class Point3d {public:    // ...private:    float x;private:    static list<Point3d*>* freeList;private:    float y;private:    static const int chunkSize = 0;private:    float z;};

Data member的存取

再来看一段代码,如下:

class Point3d {public:    float x;    static list<Point3d*>* freeList;    float y;    static int chunkSize;    float z;};int Point3d::chunkSize = 0;
然后:

int main() {    Point3d origin;    Point3d *pt = &origin;    // 下面这两天存取语句有什么差异?    origin.x = 0.0F;    pt->x = 0.0F;    system("pause");    return 0;}
这里我们要分情况讨论,从之前的学习我们知道class data member是分为static和nonstatic两种,我们就此分别进行讨论。

static data member

        正如之前所说,static data member被视为一个global的(但只在class声明范围内可见),而不论是存在多少的class object,static data member只存在一个实例,并且在没有任何class object的情况下,static data member也是存在的。也就是说其实static data member的存取并不需要通过class object就可以完成,因为它并不在class object中。实际上,我们队static data member存取操作时,如:

origin.chunkSize = 1;   // 编译器会转化为Point3d::chunkSize = 1;pt->chunkSize = 2;      // 编译器会转化为Point3d::chunkSize = 2;
因此,对于static data members,这两种存取方式并无差异。

nonstatic data member

        根据对象模型,我们知道nonstatic data members的存取是通过class object的地址加上nonstatic data members的offset(偏移)进行的。显然这个offset必须在编译期间就应该准备妥当,因此如下:

// 通过寻址进行存取,因此下面两种操作并无差异origin.x = 0.0F;        // 等价于 *(&origin + (&Point3d::x - 1)) = 0.0;pt->x = 0.0F;           // 等价于 *(pt + (&Point3d::x - 1)) = 0.0;
当然,对于那些单一继承、多重继承来的data members也是跟上面的一样,都是寻址+偏移完成。

但是,有一个叫virutal关键字我们每次看到它的时候心里就应该知道要特殊对待,这就下面要讲的内容。

继承与Data member

        在C++继承模型中,一个derived class object表现出来的东西,是自己的members与base class(es) members的总和。至于derived class member与base(es) class members的排列顺序,在C++ Standard中并未规定,由编译器自由安排之。但在大部分的编译器中,base class members总是先出现,但属于virtual base class的除外(一般而言,任何一条通则,碰到virtual base class就昧着了)。

比如:

class Concrete1 {public:    // ...private:    int val;    char bit1;};class Concrete2 : public Concrete1 {public:    // ...private:    char bit2;};class Concrete3 : public Concrete2 {public:    // ...private:    char bit3;};
它们的关系如下图:

       

上面也是我们能够预料到的结果,这样的代码写法,造成许多的内存空间被浪费。

加上多态

如下的代码:

class Point2d {public:    // has a virtual function    // ...private:    float _x;    float _y;};class Point3d : public Point2d {public:    // override or hide the functionprivate:    float _z;};

  
由于目前阶段讨论的但是data member布局,故图中没有展现member functions的内容。

多重继承

代码如下:

class Point2d {public:    // has virtual functions    // ...private:    float _x;    float _y;};class Point3d : public Point2d {public:    // ...private:    float _z;};class Vertex {public:    // has virtual functions    // ...private:    Vertex* next;};class Vertex3d : public Point3d, public Vertex {public:    // ...private:    float mumble;};

上图便是多重继承的data members的布局。

Vertex3d v3d;Vertex* pv;// 当发生这样的操作时pv = &v3d;// 其内部发生的操作伪代码为:pv = (Vertex*) ( ((char*)&v3d) + sizeof(Point3d) );
        由于data members的位置(offset)在编译时就已经准备妥当了,当我们要存取某个base class中的data member也就是计算offset这样简单的操作。

虚拟继承

        再来看一段virtual inheritance的代码,如下:

class Point2d {public:    // has virtual functions    // ...private:    float _x;    float _y;};class Point3d : public virtual Point2d {        // virtual inheritancepublic:    // ...private:    float _z;};class Vertex : public virtual Point2d {         // virtual inheritancepublic:    // has virtual functions    // ...private:    Vertex* next;};class Vertex3d : public Point3d, public Vertex {public:    // ...private:    float mumble;};
其UML图如下:

        对于virtual inheritance,它的存在就必须要支持某种形式的“shared subobject”,也就是说它只会存在一个virtual base class subobject。一般来说其对象模型会划分成两个部分:不变区域部分共享区域部分
        不变区域部分指的是,不管后继如何衍化,总是拥有固定的offset,所以这一部分区域可以被直接存取;共享区域部分,很显然指的是virtual base class subobject,这一部分其位置会因派生操作而有变化,所以只能被间接存取。
        一般来说,各家编译器的差异就在于间接存取(共享部分)的策略不同。
        一般的布局策略是先安排好derived class的不变部分,然后再建立起共享部分。对于共享部分的存取策略,下面介绍两种策略:指针策略(pointer strategy)、虚表策略(virtual table offset strategy)。
        以上面class的虚拟继承关系,对于pointer strategy而言,它们的对象模型如下:

        可以从上面的对象模型中看到,virtual base class subobject部分在最后面,而base class根据继承的顺序依次排列,并且在每一个derived class object中安插了一个指针,这个指针用来指向virtual base class subobject(共享部分),因此要对共享部分进行存取,可以通过相关指针间接完成。

        很明显,我们通过观察分析,发现这种pointer strategy对象模型存在缺点:对于每一个对象都会背负一个指向virtual base class的指针,这会导致class object的负担随着virtual base class的增加而真多,也就是说这些额外的负担是会变化的,我们并不能掌控其大小;        

针对这个问题,一般而言有两种方法:

        我们可以借鉴表格驱动模型来解决(即Microsoft编译器的方案),也就是说为有一个或多个virtual base classes的class object安插一个指针,指向virtual base class table表格,而表格中存放的是真正的virtual base class的地址。(注意也就是说,不论有多少个virtual base class,都只安插一个指针)

        第二种办法也是建立virtual base class table,但table中存放的不是地址,而是virtual base class的offset(如下图)。


        上面的每一种方法都是一种实现模型,而不是一种标准。

        一般而言,virtual base class最有效的一种运用形式就是:一个抽象的virtual base class,没有任何的data member。

参考资料

[1] 深度探索C++对象模型,[美]Stanley B. Lippman著,侯捷译;

0 1
原创粉丝点击