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 function
private:
float _z;
};
多重继承
代码如下:
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著,侯捷译;
本文深入探讨了C++中类成员变量的布局规则,包括静态成员变量与非静态成员变量的区别,以及继承(特别是多重继承和虚拟继承)对成员变量布局的影响。
365

被折叠的 条评论
为什么被折叠?



