先来看一个类,如下:
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; };
再来看一段代码,如下:
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被视为一个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 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关键字我们每次看到它的时候心里就应该知道要特殊对待,这就下面要讲的内容。
在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 function private: 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 inheritance public: // ... private: float _z; }; class Vertex : public virtual Point2d { // virtual inheritance public: // has virtual functions // ... private: Vertex* next; }; class Vertex3d : public Point3d, public Vertex { public: // ... private: float mumble; };其UML图如下:
可以从上面的对象模型中看到,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著,侯捷译;