深度探索C++对象模型(DataMember的布局)

深度探索C++对象模型(DataMember的布局)

C++ 对象模型 DataMember 的布局

一、布局

1
2
3
4
5
6
7
8
9
10
11
class Point3d
{
public:
// ...
private:
float x;
static list<Point3d *> *freeList;
float y;
static const int chunkSize = 0;
float z;
};

我们可以知道 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 的多少并不会导致额外的负担。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Point3d 
{
public:
// ...
private:
float x;

private:
static list<Point3d *> *freeList;

private:
float y;

private:
static const int chunkSize = 0;

private:
float z;
};

二、存取

再来看一段代码,如下:

1
2
3
4
5
6
7
8
9
10
11
class Point3d 
{
public:
float x;
static list<Point3d *> *freeList;
float y;
static int chunkSize;
float z;
};

int Point3d::chunkSize = 0;

然后:

1
2
3
4
5
6
7
8
9
10
11
int main() {
Point3d origin;
Point3d *pt = &origin;

// 下面这两天存取语句有什么差异?
origin.x = 0.0F;
pt->x = 0.0F;

system("pause");
return 0;
}

这里我们要分情况讨论,从之前的学习我们知道 class data member 是分为 static 和 nonstatic 两种,我们就此分别进行讨论。

2.1、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 存取操作时,如:

1
2
origin.chunkSize = 1;   // 编译器会转化为 Point3d::chunkSize = 1;
pt->chunkSize = 2; // 编译器会转化为 Point3d::chunkSize = 2;

因此,对于 static data members,这两种存取方式并无差异。

2.2、nonstatic data member

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

1
2
3
// 通过寻址进行存取,因此下面两种操作并无差异
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 的除外。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class Concrete1
{
public:

// ...

private:
int val;
char bit1;
};

class Concrete2 : public Concrete1
{
public:

// ...

private:
char bit2;
};

class Concrete3 : public Concrete2
{
public:

// ...

private:
char bit3;
};

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

3.1、加上多态

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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;
};

3.2、多重继承

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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 的布局。

1
2
3
4
5
Vertex3d v3d;
Vertex *pv;
// 当发生这样的操作时
pv = &v3d;
// 其内部发生的操作伪代码为:pv = (Vertex*) ( ((char*)&v3d) + sizeof(Point3d) );

由于 data members 的位置(offset)在编译时就已经准备妥当了,当我们要存取某个 base class 中的 data member 也就是计算 offset 这样简单的操作。

3.3、虚拟继承

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
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;
};

对于 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。

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

评论

:D 一言句子获取中...

加载中,最新评论有1分钟缓存...