初探类的内存布局与虚函数表

Reference

https://blog.twofei.com/496/

http://www.cnblogs.com/QG-whz/p/4909359.html#_label1

前言

今天学习了虚函数, 知道虚函数对Cpp有十分重要的意义, 在参考了网上的一篇文章后, 决定自己仿照她的流程, 独立地研究一遍Cpp虚函数的机制.

虚函数的作用:

  • 实现多态(动态绑定)
  • 接口函数

如何查看Cpp对象的内存布局

在VS2017有以下方法获取对象的内存布局

  • 使用VS的开发人员命令行工具, 到源文件目录键入
    cl /d1 reportSingleClassLayout类名 "文件名"

  • 通过offetof宏输出成员变量的偏移

  • 通过调试器查看
    在调试模式下设置断点后可以借助监视窗口查看

  • 在VS资源管理器右击源文件, 打开其属性窗口, 在 [配置属性 -> C/C++ -> 命令行] 中的其他选项窗口中添加如下字段
    /d1 reportAllClassLayout r
    然后每次在Debug模式下调试都会在VS的输出窗口输出类的内存布局
    注意: VS的监视工具显示的顺序不是类的内存布局顺序

此外, 在linux环境下也有相应的工具, 比如gdb

那么不再赘述, 让我们开始探索之旅吧!

Cpp对象的内存布局

只含成员对象的对象

类实现如下:

1
2
3
4
5
6
class Base1
{
public:
    int _data_1;
    int _data_2;
};

对象大小及偏移:

1
2
3
4
5
1>class Base1   size(8):
1>   +---
1> 0    | _data_1
1> 4    | _data_2
1>   +---

成员对象在内存中的顺序与声明顺序一致, 类对象的大小在没有虚成员函数时即为其非静态成员对象的大小之和. (但是任何对象或成员子对象的大小至少为1, 即使该类型是空类类型, 但是由于空基类优化, 基类子对象大小实际也可能为0)

只含成员对象和非虚成员函数的对象

类实现如下:

1
2
3
4
5
6
7
8
9
10
11
class Base2
{
public:
    int _data_1;
    int _data_2;

    void foo()
    {

    }
};

对象大小及偏移:

1
2
3
4
5
1>class Base2   size(8):
1>      +---
1> 0    | _data_1
1> 4    | _data_2
1>      +---

只有一个虚函数的类对象

类实现如下:

1
2
3
4
5
6
7
8
class Base3
{
public:
    int _data_1;
    int _data_2;

    virtual void foo() {}
};

对象大小及偏移:

1
2
3
4
5
6
1>class Base3   size(12):
1>   +---
1> 0    | {vfptr}
1> 4    | _data_1
1> 8    | _data_2
1>   +---

虚函数表:

1
2
3
4
5
6
1>Base3::$vftable@:
1>   | &Base3_meta
1>   |  0
1> 0    | &Base3::foo
1>
1>Base3::foo this adjustor: 0

为了更确切的观察内存布局, 我们需要看看{vfptr}的类型

内存布局:

_vfptr  _data_l  _ data_2  OxOOa 17b34 (Cpp {OXOOa I I Ife {Cpp  oxooa 111 fe (Cpp  -858993460  858993460  void

Base3相比Base1和Base2有如下不同:

  • 类对象大小增大4
  • 类中有一个新成员对象 {vfptr}, 它是一个指向一个void*指针数组的指针

这里的数组就是虚函数表(vtable), 而这个变量__vfptr即为指向虚函数表的指针(vtable指针).

这里vfptr的类型为void*而不是void[0]隐含了如下意义: 这一片内存区域不属于Base3类, vfptr仅仅是一个指向虚函数表的指针.

我们来看看[0], 它的值是0x00a111fe {Cpp Primer.exe!Base3::foo(void)}, 这表示函数Base3::foo(void)的地址.

此外, 我们注意到虚表的大小是[2], 这是因为虚函数表末尾有一个终止标记.

拥有多个虚函数的类对象

类定义如下:

1
2
3
4
5
6
7
8
9
class Base4
{
public:
    int _data_1;
    int _data_2;

    virtual void foo1() {}
    virtual void foo2() {}
};

对象大小及偏移:

1
2
3
4
5
6
1>class Base4   size(12):
1>   +---
1> 0    | {vfptr}
1> 4    | _data_1
1> 8    | _data_2
1>   +---

虚函数表:

1
2
3
4
5
6
1>Base4::$vftable@:
1>   | &Base4_meta
1>   |  0
1> 0    | &Base4::foo1
1> 1    | &Base4::foo2
1>

内存布局:

1530609826283

这也印证了我们之前的想法虚函数表是一个函数指针数数组

更进一步地, 我们声明两个Base4类型的对象

1530609842302

我猜测虚函数表和虚函数应该都和普通函数一样在静态区上的代码区

单继承且本身不存在虚函数的派生类对象

类定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Base5
{
public:
    int base1_1;
    int base1_2;

    virtual void base1_fun1() {}
    virtual void base1_fun2() {}
};

class Derive5 : public Base5
{
public:
    int derive1_1;
    int derive1_2;
};

对象大小及偏移:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
1>class Base5   size(12):
1>   +---
1> 0    | {vfptr}
1> 4    | base1_1
1> 8    | base1_2
1>   +---

1>class Derive5 size(20):
1>   +---
1> 0    | +--- (base class Base5)
1> 0    | | {vfptr}
1> 4    | | base1_1
1> 8    | | base1_2
1>   | +---
1>12    | derive1_1
1>16    | derive1_2
1>   +---

虚函数表:

1
2
3
4
5
6
7
8
9
10
11
1>Base5::$vftable@:
1>   | &Base5_meta
1>   |  0
1> 0    | &Base5::base1_fun1
1> 1    | &Base5::base1_fun2

1>Derive5::$vftable@:
1>   | &Derive5_meta
1>   |  0
1> 0    | &Base5::base1_fun1
1> 1    | &Base5::base1_fun2

内存布局:

1530609891977

显然, 派生类的内存布局如下(基类子对象+成员对象)

值得注意的是, 这里基类的虚函数表vtable_1和派生类中的基类子对象的虚函数表vtable_2是不同的, 比较如下:

  • vtable_1和vtable_2指向两个不同的函数指针数组
  • 由于派生类未对虚函数重写(overwrite), 所以基类的虚函数未被隐藏, vtable2指向的虚函数表依旧包含的是基类虚函数的指针

这也恰解释了为什么使用dynamic_cast要求基类至少有一个虚函数, 因为可以借助对象在内存中存放的虚函数表指针的不同来区分对象的类型.

我们这里再做一个小实验进一步印证我的想法:

类定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Base5
{
public:
    int base1_1;
    int base1_2;

    virtual void base1_fun1() {}
    virtual void base1_fun2() {}
};

class Derive5 : public Base5
{
public:
    int derive1_1;
    int derive1_2;
};

class Derive5_ : public Derive5
{
public:
    int derive5_1;
    int derive5_2;
};

内存布局:

1530609932110

本身没新声明虚函数但覆盖了基类虚函数的单继承类

类定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Base6
{
public:
    int base6_1;
    int base6_2;

    virtual void base6_fun1() {}
    virtual void base6_fun2() {}
};

class Derive6 : public Base6
{
public:
    int derive6_1;
    int derive6_2;

    // 覆盖基类函数
    virtual void base6_fun1() {}
};

内存布局:

1530609949990

我们注意到d6 -> Base6 -> __vfptr -> [0] 发生了变化, 这也恰巧是存放被覆盖的base6_fun1函数指针的位置

本身新定义了虚函数的单继承派生类

类定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Base7
{
public:
    int base7_1;
    int base7_2;

    virtual void base7_fun1() {}
    virtual void base7_fun2() {}
};

class Derive7 : public Base7
{
public:
    int derive7_1;
    int derive7_2;

    virtual void derive7_fun1() {}
};

内存布局:

1530609967583

我们立即注意到d7 -> Base7 -> __vfptr相比d6, 虚函数表的大小从[3]变为[4], 而对于Base7类来说这个多出来的部分[2]是不可见的, 这个[2]存放了Derive7::derive7_fun1的指针, 显然, 派生类的虚函数表被拼接到基类虚函数表之后

我们借助汇编代码观察

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    Derive7 d7;
00C518CE  lea         ecx,[d7]  
00C518D1  call        Derive7::Derive7 (0C513BBh)  
    Derive7* pd7 = &d7;
00C518D6  lea         eax,[d7]  
00C518D9  mov         dword ptr [pd7],eax  
    pd7->derive7_fun1();
00C518DC  mov         eax,dword ptr [pd7] //取出pd7的右值(对象地址), 并以此作为eax的左值
00C518DF  mov         edx,dword ptr [eax] //取出eax的右值(虚表地址), 并以此作为edx的左值
00C518E1  mov         esi,esp  
00C518E3  mov         ecx,dword ptr [pd7] //__thiscall调用, this参数入栈
00C518E6  mov         eax,dword ptr [edx+8]  //取出虚表中第三个元素, 以此作为eax左值
00C518E9  call        eax   //调用虚函数
00C518EB  cmp         esi,esp  
00C518ED  call        __RTC_CheckEsp (0C5113Bh)

这里提醒一句, 要实现多态需要用指针或引用调用函数.

总结:

  • 派生类的虚表被拼接到其基类子对象虚表之后
  • 基类子对象对虚表被拼接的部分不知情

多继承且存在虚函数覆盖和新定义的虚函数的派生类

类定义如下:

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
class Base8_2
{
public:
    int base8_2_1;
    int base8_2_2;

    virtual void base8_2_fun1() {}
    virtual void base8_2_fun2() {}
};

// 多继承
class Derive8 : public Base8_1, public Base8_2
{
public:
    int derive8_1;
    int derive8_2;

    // 基类虚函数覆盖
    virtual void base8_1_fun1() {}
    virtual void base8_2_fun2() {}

    // 自身定义的虚函数
    virtual void derive8_fun1() {}
    virtual void derive8_fun2() {}
};

内存布局:

1530610070148

我们立刻注意到d8 -> Base8_1 -> __vfptr的大小从[3]变为[5], 我们结合上一个实验, 知晓Derive8的新声明虚函数被拼接到其后

总结

  • 基类子对象按照基类声明顺序, 依次分布在派生类对象首部
  • 派生类虚函数被拼接到第一个虚函数表后

如果第一个直接基类没有虚函数(表)

类定义如下:

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
class Base9_1
{
public:
    int base9_1_1;
    int base9_1_2;
};

class Base9_2
{
public:
    int base9_2_1;
    int base9_2_2;

    virtual void base9_2_fun1() {}
    virtual void base9_2_fun2() {}
};

// 多继承
class Derive9 : public Base9_1, public Base9_2
{
public:
    int derive9_1;
    int derive9_2;

    // 自身定义的虚函数
    virtual void derive9_fun1() {}
    virtual void derive9_fun2() {}
};

内存布局:

1530610164743

虽然和上次一样但是需要借助偏移查看工具:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Derive9   size(28):
        +---
0       | +--- (base class Base9_2)
0       | | {vfptr}
4       | | base9_2_1
8       | | base9_2_2
        | +---
12      | +--- (base class Base9_1)
12      | | base9_1_1
16      | | base9_1_2
        | +---
20      | derive9_1
24      | derive9_2
        +---

Derive9::$vftable@:
        | &Derive9_meta
        |  0
0       | &Base9_2::base9_2_fun1
1       | &Base9_2::base9_2_fun2
2       | &Derive9::derive9_fun1
3       | &Derive9::derive9_fun2

显然我们注意到Base9_2被移动到Derive9对象的首部, 其实这是出于效率考虑编译器会尽量使虚函数表在对象首部.

如果两个基类都没有虚函数表

类定义如下:

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
class Base10_1
{
public:
    int base10_1_1;
    int base10_1_2;
};

class Base10_2
{
public:
    int base10_2_1;
    int base10_2_2;
};

// 多继承
class Derive10 : public Base10_1, public Base10_2
{
public:
    int derive10_1;
    int derive10_2;

    // 自身定义的虚函数
    virtual void derive10_fun1() {}
    virtual void derive10_fun2() {}
};

内存布局:

1530610197775

偏移:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
1>class Derive10    size(28):
1>   +---
1> 0    | {vfptr}
1> 4    | +--- (base class Base10_1)
1> 4    | | base10_1_1
1> 8    | | base10_1_2
1>   | +---
1>12    | +--- (base class Base10_2)
1>12    | | base10_2_1
1>16    | | base10_2_2
1>   | +---
1>20    | derive10_1
1>24    | derive10_2
1>   +---
1>
1>Derive10::$vftable@:
1>   | &Derive10_meta
1>   |  0
1> 0    | &Derive10::derive10_fun1
1> 1    | &Derive10::derive10_fun2

显然虚函数表还是被移动到了最前面

如果三个基类分别有, 无, 有虚函数

类定义如下:

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
class Base11_1
{
public:
    int base11_1_1;
    int base11_1_2;

    virtual void base11_1_fun1() {}
    virtual void base11_1_fun2() {}
};

class Base11_2
{
public:
    int base11_2_1;
    int base11_2_2;
};

class Base11_3
{
public:
    int base11_3_1;
    int base11_3_2;

    virtual void base11_3_fun1() {}
    virtual void base11_3_fun2() {}
};

// 多继承
class Derive11 : public Base11_1, public Base11_2, public Base11_3
{
public:
    int derive11__1;
    int derive11__2;

    // 自身定义的虚函数
    virtual void derive11__fun1() {}
    virtual void derive11__fun2() {}
};

内存布局:

1530610222770

偏移:

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
1>class Derive11    size(40):
1>   +---
1> 0    | +--- (base class Base11_1)
1> 0    | | {vfptr}
1> 4    | | base11_1_1
1> 8    | | base11_1_2
1>   | +---
1>12    | +--- (base class Base11_3)
1>12    | | {vfptr}
1>16    | | base11_3_1
1>20    | | base11_3_2
1>   | +---
1>24    | +--- (base class Base11_2)
1>24    | | base11_2_1
1>28    | | base11_2_2
1>   | +---
1>32    | derive11__1
1>36    | derive11__2
1>   +---
1>
1>Derive11::$vftable@Base11_1@:
1>   | &Derive11_meta
1>   |  0
1> 0    | &Base11_1::base11_1_fun1
1> 1    | &Base11_1::base11_1_fun2
1> 2    | &Derive11::derive11__fun1
1> 3    | &Derive11::derive11__fun2
1>
1>Derive11::$vftable@Base11_3@:
1>   | -12
1> 0    | &Base11_3::base11_3_fun1
1> 1    | &Base11_3::base11_3_fun2

派生类虚函数表被拼接到第一个虚函数表后

且有虚函数的基类子对象被优先移动到派生类对象首部

代码

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
class Base1
{
public:
int _data_1;
int _data_2;
};

class Base2
{
public:
int _data_1;
int _data_2;

void foo()
{

}
};

class Base3
{
public:
int _data_1;
int _data_2;

virtual void foo() {}
};

class Base4
{
public:
int _data_1;
int _data_2;

virtual void foo1() {}
virtual void foo2() {}
};



class Base5
{
public:
int base1_1;
int base1_2;

virtual void base1_fun1() {}
virtual void base1_fun2() {}
};

class Derive5 : public Base5
{
public:
int derive1_1;
int derive1_2;
};

class Derive5_ : public Derive5
{
public:
int derive5_1;
int derive5_2;
};

class Base6
{
public:
int base6_1;
int base6_2;

virtual void base6_fun1() {}
virtual void base6_fun2() {}
};

class Derive6 : public Base6
{
public:
int derive6_1;
int derive6_2;

// 覆盖基类函数
virtual void base6_fun1() {}
};

class Base7
{
public:
int base7_1;
int base7_2;

virtual void base7_fun1() {}
virtual void base7_fun2() {}
};

class Derive7 : public Base7
{
public:
int derive7_1;
int derive7_2;

virtual void base7_fun1() {}
};

class Base8_1
{
public:
int base8_1_1;
int base8_1_2;

virtual void base8_1_fun1() {}
virtual void base8_1_fun2() {}
};

class Base8_2
{
public:
int base8_2_1;
int base8_2_2;

virtual void base8_2_fun1() {}
virtual void base8_2_fun2() {}
};

// 多继承
class Derive8 : public Base8_1, public Base8_2
{
public:
int derive8_1;
int derive8_2;

// 基类虚函数覆盖
virtual void base8_1_fun1() {}
virtual void base8_2_fun2() {}

// 自身定义的虚函数
virtual void derive8_fun1() {}
virtual void derive8_fun2() {}
};

class Base9_1
{
public:
int base9_1_1;
int base9_1_2;
};

class Base9_2
{
public:
int base9_2_1;
int base9_2_2;

virtual void base9_2_fun1() {}
virtual void base9_2_fun2() {}
};

// 多继承
class Derive9 : public Base9_1, public Base9_2
{
public:
int derive9_1;
int derive9_2;

// 自身定义的虚函数
virtual void derive9_fun1() {}
virtual void derive9_fun2() {}
};

class Base10_1
{
public:
int base10_1_1;
int base10_1_2;
};

class Base10_2
{
public:
int base10_2_1;
int base10_2_2;
};

// 多继承
class Derive10 : public Base10_1, public Base10_2
{
public:
int derive10_1;
int derive10_2;

// 自身定义的虚函数
virtual void derive10_fun1() {}
virtual void derive10_fun2() {}
};

class Base11_1
{
public:
int base11_1_1;
int base11_1_2;

virtual void base11_1_fun1() {}
virtual void base11_1_fun2() {}
};

class Base11_2
{
public:
int base11_2_1;
int base11_2_2;
};

class Base11_3
{
public:
int base11_3_1;
int base11_3_2;

virtual void base11_3_fun1() {}
virtual void base11_3_fun2() {}
};

// 多继承
class Derive11 : public Base11_1, public Base11_2, public Base11_3
{
public:
int derive11__1;
int derive11__2;

// 自身定义的虚函数
virtual void derive11__fun1() {}
virtual void derive11__fun2() {}
};

class Base12
{
public:
int base12_1;
int base12_2;

virtual void base12_fun1() = 0;
};

class Derive12 : public Base12
{
public:
int derive12_1;
int derive12_2;

virtual void base12_fun1()
{

}
};

int main()
{

}
文章目录
  1. 1. Reference
  2. 2. 前言
  3. 3. 如何查看Cpp对象的内存布局
  4. 4. Cpp对象的内存布局
    1. 4.1. 只含成员对象的对象
    2. 4.2. 只含成员对象和非虚成员函数的对象
    3. 4.3. 只有一个虚函数的类对象
    4. 4.4. 拥有多个虚函数的类对象
    5. 4.5. 单继承且本身不存在虚函数的派生类对象
    6. 4.6. 本身没新声明虚函数但覆盖了基类虚函数的单继承类
    7. 4.7. 本身新定义了虚函数的单继承派生类
    8. 4.8. 多继承且存在虚函数覆盖和新定义的虚函数的派生类
    9. 4.9. 如果第一个直接基类没有虚函数(表)
    10. 4.10. 如果两个基类都没有虚函数表
    11. 4.11. 如果三个基类分别有, 无, 有虚函数
    12. 4.12. 代码
|