C++对象内存模型
2400 Words | Read in about 12 Min | View times
Overview
C++的三大特性是封装、继承和多态,而要理解这些特性的底层原理,就不得不说说C++对象的内存模型(布局)。
对于一个C++对象,每个对象有独立的非静态数据成员,而内存中只有一份成员函数,所有该类的对象共享成员函数;static
数据成员属于类,它们存储在静态存储区,该类的所有对象共享。当调用对象的成员函数时,又是怎么识别是哪个对象在调用呢?实际上,所有类的成员函数在编译期会被编译器重构成非成员函数,即将this
指针作为函数的第一个参数,这样在函数中通过this
指针就能找到属于该对象的其他数据成员了。这些都是理解C++对象内存模型的前提。
当出现了类的继承关系,且存在虚函数的时候,情况就复杂了。本节内容将详细介绍单一继承、多重继承、重复继承、虚拟继承等不同的继承方式的对象内存模型。
本系列文章将包括以下领域:
本章其他内容请见 《现代C++》
开胃小菜
我们知道在C++中的多态,是通过虚函数表来实现的。那怎样从代码中,通过对象的地址来获取虚函数表的地址呢?
注意:本节所有结论是代码运行在64位机器的gcc10.2得出的。
1typedef void(*Func)(void);
2
3Base b;
4
5std::cout << "虚表地址:" << *(intptr_t*)&b << std::endl;
6std::cout << "第1个虚表函数地址: " << *((intptr_t*)*(intptr_t*)&b + 0) << std::endl;
7std::cout << "第2个虚表函数地址: " << *((intptr_t*)*(intptr_t*)&b + 1) << std::endl;
8std::cout << "第3个虚表函数地址: " << *((intptr_t*)*(intptr_t*)&b + 2) << std::endl;
9
10/*
11解释一下上面这段取地址的代码
12- &b:代表对象b的起始地址
13- (intptr_t*)&b:强转成intptr_t*类型,目的是为了接下来取内存空间偏移量为0的元素,即虚表指针vptr
14- *(intptr_t*)&b:vptr虚表地址
15*/
16
17//调用虚函数
18auto foo = (Func)(*((intptr_t*)*(intptr_t*)&b + 0));
19auto bar = (Func)(*((intptr_t*)*(intptr_t*)&b + 1));
20auto baz = (Func)(*((intptr_t*)*(intptr_t*)&b + 2));
21foo();
22bar();
23baz();
24
25/*
26解释一下上面这段代码
27- 由于*(intptr_t*)&b是虚表,存放着虚函数指针的地址,所以虚表每个元素代表一个虚函数指针
28- (intptr_t*)*(intptr_t*)&b + 0:虚表地址强转为intptr_t*类型,就得到第1个虚函数指针,解引用后再强转为Func即第一个虚函数
29- (intptr_t*)*(intptr_t*)&b + 1:下1个虚函数指针,解引用后再强转为Func即第二个虚函数
30- 依次类推
31*/
32
33auto type_info = (std::type_info*)(*((intptr_t*)*(intptr_t*)&b - 1));
34std::cout << "type_info: " << typeinfo->name() << std::endl; //4Base
35std::cout << "typeid: " << typeid(b).name() << std::endl; //4Base
36
37/*
38解释一下上面这段代码
39- (intptr_t*)*(intptr_t*)&b + 0:虚表地址强转为intptr_t*类型,就得到第1个虚函数指针
40- (intptr_t*)*(intptr_t*)&b - 1:type_info指针一般都位于虚表的偏移量为-1的位置,所以这个地址解引用后再强转为std::type_info*类型即type_info的指针
41*/
请仔细阅读上述代码的注释,这样你会清楚如何通过对象地址获得各个虚函数的地址以及type_info
对象的地址。关于type_info
我们还将在后面RTTI与反射的章节中介绍。
我们可以通过这种方式来获取整个对象的内存布局,这些结构在内存中都是连续分布的,我们只需要知道地址偏移量,就可以得到具体的对象数据或函数。
gcc编译器还提供一个命令,可以用来导出内存布局结构:
1>$ g++ -fdump-lang-class main.cpp
执行后会生成一个*.001l.class
后缀的文件,其中可以查看对象的内存布局,比如
1Vtable for D
2D::_ZTV1D: 20 entries
30 32
48 (int (*)(...))0
516 (int (*)(...))(& _ZTI1D)
624 (int (*)(...))D::foo
732 (int (*)(...))D::baz
840 (int (*)(...))B::qux
948 (int (*)(...))D::quux
1056 (int (*)(...))D::corge
1164 16
1272 (int (*)(...))-16
1380 (int (*)(...))(& _ZTI1D)
1488 (int (*)(...))D::_ZThn16_N1D3fooEv
1596 (int (*)(...))D::_ZThn16_N1D4quuxEv
16104 (int (*)(...))C::quuz
17112 0
18120 18446744073709551584
19128 (int (*)(...))-32
20136 (int (*)(...))(& _ZTI1D)
21144 (int (*)(...))D::_ZTv0_n24_N1D3fooEv
22152 (int (*)(...))A::bar
单一继承
假设有如下图的继承关系:
对应的源代码为:
1class A {
2public:
3 A() : m1(1) {}
4 virtual void foo_A() { std::cout << "A::foo_A()" << std::endl; }
5 virtual void bar_A() { std::cout << "A::bar_A()" << std::endl; }
6 virtual void baz_A() { std::cout << "A::baz_A()" << std::endl; }
7
8 int m1;
9};
10
11class B : public A {
12public:
13 B() : m2(2) {}
14 virtual void foo_A() { std::cout << "B::foo_A()" << std::endl; }
15 virtual void bar_B() { std::cout << "B::bar_B()" << std::endl; }
16 virtual void baz_B() { std::cout << "B::baz_B()" << std::endl; }
17
18 int m2;
19};
20
21class C : public B {
22public:
23 C() : m3(3) {}
24 virtual void foo_A() { std::cout << "C::foo_A()" << std::endl; }
25 virtual void bar_B() { std::cout << "C::bar_B()" << std::endl; }
26 virtual void baz_C() { std::cout << "C::baz_C()" << std::endl; }
27
28 int m3;
29};
A
、B
、C
三个类各有一个自己的成员变量,B
类覆盖了父类的foo_A
虚函数,新增了虚函数bar_B
和baz_B
;C
类覆盖了父类的foo_A
和bar_B
虚函数,新增了虚函数bar_C
。
接下来我们通过一个测试程序来打印C
类的内存布局:
1typedef void(*Func)(void);
2
3int main() {
4 C c;
5
6 //vptr是指针,64位系统上指针的大小为8,注意int的大小仍为4而不是8,long才是8
7 intptr_t** ptab = reinterpret_cast<intptr_t**>(&c);
8
9 std::cout << "[0] " << ptab << " C::vptr->" << std::endl;
10 for (int i = 0; i < 6; ++i) {
11 auto func = reinterpret_cast<Func>(*(*ptab + i));
12 std::cout << " [" << i << "] " << *ptab + i << ": ";
13 func();
14 }
15
16 //先取到成员数据的起始地址,再注意取出成员变量
17 int* p = reinterpret_cast<int*>(reinterpret_cast<intptr_t*>(ptab + 1));
18
19 std::cout << "[1] " << p + 0 << " C.m1 = " << *(p + 0) << std::endl;
20 std::cout << "[2] " << p + 1 << " C.m2 = " << *(p + 1) << std::endl;
21 std::cout << "[3] " << p + 2 << " C.m3 = " << *(p + 2) << std::endl;
22
23 return 0;
24}
我们需要解释一下上面这段代码。
intptr_t** ptab = reinterpret_cast<intptr_t**>(&c)
&c
是对象c的起始地址,也是虚表指针vptr
的地址,而vptr
的指本身又是一个指针,指向一段连续空间的虚函数表,所以本质上vptr
可以看作一个二级指针。注意地址和指针是可以相互转换的,reinterpret_cast<T>
的作用可以参考之前的文章《类型转换》
reinterpret_cast<Func>(*(*ptab + i))
*ptab
是虚表空间的第一个元素,即第一个虚函数指针。*ptab + i
代表虚表里每个虚函数指针。*(*ptab + i)
解引用得到第i个虚函数的地址,再转成Func
类型后就代表了一个函数,可以直接调用了。
reinterpret_cast<int*>(reinterpret_cast<intptr_t*>(ptab + 1))
由于vptr
的大小是8,而接下来3个成员变量的大小均为4,我们先定位到第一个成员变量的地址,即ptab + 1
。将地址转为指针后,由于我们之前都是使用intptr_t*
类型的指针,其大小是8,我们接下来应该将其重新按大小为4的int*
指针来解释,后面的3个成员变量才能用这个指针迭代获取。
代码的运行结果如下:
1[0] 0x7ffe4c28f740 C::vptr->
2 [0] 0x557777e43c98: C::foo_A()
3 [1] 0x557777e43ca0: A::bar_A()
4 [2] 0x557777e43ca8: A::baz_A()
5 [3] 0x557777e43cb0: C::bar_B()
6 [4] 0x557777e43cb8: B::baz_B()
7 [5] 0x557777e43cc0: C::baz_C()
8[1] 0x7ffe4c28f748 C.m1 = 1
9[2] 0x7ffe4c28f74c C.m2 = 2
10[3] 0x7ffe4c28f750 C.m3 = 3
仔细观察,vptr
的地址0x7ffe4c28f740
与第一个成员变量m1
的地址0x7ffe4c28f748
相差是8个字节,说明vptr
大小是8;而m1
、m2
、m3
的地址差都是4个字节,即每个int的大小是4字节。
该运行结果可以用下图直观的展示出来:
**图中top_offset
指的是虚表指针地址与对象起始地址的偏移量。**关于top_offset
的含义我们先按下不表,最后我们再回头来看。
多重继承
假如有如下图的继承关系:
1class A {
2public:
3 A() : m1(1) {}
4 virtual void foo() { std::cout << "A::foo()" << std::endl; }
5 virtual void bar() { std::cout << "A::bar()" << std::endl; }
6 virtual void baz() { std::cout << "A::baz()" << std::endl; }
7 int m1;
8};
9
10class B {
11public:
12 B() : m2(2) {}
13 virtual void foo() { std::cout << "B::foo()" << std::endl; }
14 virtual void bar() { std::cout << "B::bar()" << std::endl; }
15 virtual void baz() { std::cout << "B::baz()" << std::endl; }
16 int m2;
17};
18
19class C : public A, public B {
20public:
21 C() : m3(3) {}
22 virtual void foo() { std::cout << "C::foo()" << std::endl; }
23 virtual void qux() { std::cout << "C::qux()" << std::endl; }
24 int m3;
25};
A
、B
、C
三个类各有一个自己的成员变量,C
类多继承了A
类和B
类,C
类的foo
覆盖了父类的虚函数,新增了虚函数qux
。
接下来我们通过一个测试程序来打印C
类的内存布局:
1typedef void(*Func)(void);
2
3int main() {
4 C c;
5
6 intptr_t** ptaba = reinterpret_cast<intptr_t**>(&c);
7 std::cout << "[0] " << ptaba << " A::vptr->" << std::endl;
8 for (int i = 0; i < 5; ++i) {
9 auto func = reinterpret_cast<Func>(*(*ptaba + i));
10 std::cout << " [" << i << "] " << *ptaba + i << ": ";
11 if (4 == i)
12 std::cout << func << std::endl;
13 else
14 func();
15 }
16
17 int* p = reinterpret_cast<int*>(reinterpret_cast<intptr_t*>(ptaba + 1));
18 std::cout << "[1] " << p + 0 << " A.m1 = " << *(p + 0) << std::endl;
19
20 intptr_t** ptabb = ptaba + sizeof(A) / sizeof(intptr_t);
21 std::cout << "[2] " << ptabb << " B::vptr->" << std::endl;
22 for (int i = 0; i < 4; ++i) {
23 auto func = reinterpret_cast<Func>(*(*ptabb + i));
24 std::cout << " [" << i << "] " << *ptabb + i << ": ";
25 if (3 == i)
26 std::cout << func << std::endl;
27 else
28 func();
29 }
30
31 int* p2 = reinterpret_cast<int*>(reinterpret_cast<intptr_t*>(ptabb + 1));
32 std::cout << "[3] " << p2 + 0 << " B.m2 = " << *(p2 + 0) << std::endl;
33 std::cout << "[4] " << p2 + 1 << " C.m3 = " << *(p2 + 1) << std::endl;
34
35 return 0;
36}
代码的运行结果如下:
1[0] 0x7ffde775b180 A::vptr->
2 [0] 0x55d3637dec80: C::foo()
3 [1] 0x55d3637dec88: A::bar()
4 [2] 0x55d3637dec90: A::baz()
5 [3] 0x55d3637dec98: C::qux() #<-- 注意这里,C类新增的虚函数直接追加到第一个父类的虚表空间中,且不会出现在第二个父类的虚表空间中
6 [4] 0x55d3637deca0: 1 #<-- 注意这里为1,表示多继承父类还未结束
7[1] 0x7ffde775b188 A.m1 = 1
8[2] 0x7ffde775b190 B::vptr->
9 [0] 0x55d3637decb0: C::foo()
10 [1] 0x55d3637decb8: B::bar()
11 [2] 0x55d3637decc0: B::baz()
12 [3] 0x55d3637decc8: 0 #<-- 注意这里为0,表示多继承父类已结束
13[3] 0x7ffde775b198 B.m2 = 2
14[4] 0x7ffde775b19c C.m3 = 3
运行结果表明,多重继承的情况下:
-
子类将拥有多个虚表指针,每个父类有一个虚表指针,按继承的顺序排列
-
被子类覆盖的虚函数,在多个虚函数表中都会替换,这样所有父类的指针指向同一个子类对象时,都能够调用到被覆盖的虚函数
-
子类有但父类没有的虚函数直接追加到第一个父类的虚表空间中,第二个父类的虚表空间不会出现
-
每个父类的虚表空间的最后一个虚函数之后,还有一个标记,该标记的值除了最后一个父类为0之外,其余都是1,当然在不同编译器的结果可能不一样,此处是gcc编译器的结果。
该运行结果可以用下图直观的展示出来:
重复继承
假如有如下图的继承关系:
1class A {
2public:
3 A() : m1(1) {}
4 virtual void foo() { std::cout << "A::foo()" << std::endl; }
5 virtual void bar() { std::cout << "A::foo()" << std::endl; }
6 int m1;
7};
8
9class B : public A {
10public:
11 B() : m2(2) {}
12 virtual void foo() { std::cout << "B::foo()" << std::endl; }
13 virtual void baz() { std::cout << "B::baz()" << std::endl; }
14 virtual void qux() { std::cout << "B::qux()" << std::endl; }
15 int m2;
16};
17
18class C : public A {
19public:
20 C() : m3(3) {}
21 virtual void foo() { std::cout << "C::foo()" << std::endl; }
22 virtual void quux() { std::cout << "C::quux()" << std::endl; }
23 virtual void quuz() { std::cout << "C::quuz()" << std::endl; }
24 int m3;
25};
26
27class D : public B, public C {
28public:
29 D() : m4(4) {}
30 virtual void foo() { std::cout << "D::foo()" << std::endl; }
31 virtual void baz() { std::cout << "D::baz()" << std::endl; }
32 virtual void quux() { std::cout << "D::quux()" << std::endl; }
33 virtual void corge() { std::cout << "D::corge()" << std::endl; }
34 int m4;
35};
A
类既是B
类的父类,也是C
类的父类,而D
类同时继承B
类和C
类。那么A
类中的成员变量和虚函数是否会在D
类中出现多次呢?
接下来我们通过一个测试程序来打印D
类的内存布局:
1typedef void(*Func)(void);
2
3int main() {
4 D d;
5
6 intptr_t** ptabb = reinterpret_cast<intptr_t**>(&d);
7 std::cout << "[0] " << ptabb << " D::B::vptr->" << std::endl;
8 for (int i = 0; i < 7; ++i) {
9 auto func = reinterpret_cast<Func>(*(*ptabb + i));
10 std::cout << " [" << i << "] " << *ptabb + i << ": ";
11 if (6 == i)
12 std::cout << func << std::endl;
13 else
14 func();
15 }
16
17 int* p = reinterpret_cast<int*>(reinterpret_cast<intptr_t*>(ptabb + 1));
18 std::cout << "[1] " << p + 0 << " A.m1 = " << *(p + 0) << std::endl;
19 std::cout << "[2] " << p + 1 << " B.m2 = " << *(p + 1) << std::endl;
20
21 intptr_t** ptabc = ptabb + sizeof(B) / sizeof(intptr_t);
22 std::cout << "[3] " << ptabc << " D::C::vptr->" << std::endl;
23 for (int i = 0; i < 5; ++i) {
24 auto func = reinterpret_cast<Func>(*(*ptabc + i));
25 std::cout << " [" << i << "] " << *ptabc + i << ": ";
26 if (4 == i)
27 std::cout << func << std::endl;
28 else
29 func();
30 }
31
32 int* p2 = reinterpret_cast<int*>(reinterpret_cast<intptr_t*>(ptabc + 1));
33 std::cout << "[4] " << p2 + 0 << " A.m1 = " << *(p2 + 0) << std::endl;
34 std::cout << "[5] " << p2 + 1 << " C.m3 = " << *(p2 + 1) << std::endl;
35 std::cout << "[6] " << p2 + 2 << " D.m4 = " << *(p2 + 2) << std::endl;
36
37 return 0;
38}
代码的运行结果如下:
1[0] 0x7ffd881e77d0 D::B::vptr->
2 [0] 0x55636d6f5c18: D::foo()
3 [1] 0x55636d6f5c20: A::bar()
4 [2] 0x55636d6f5c28: D::baz()
5 [3] 0x55636d6f5c30: B::qux()
6 [4] 0x55636d6f5c38: D::quux()
7 [5] 0x55636d6f5c40: D::corge()
8 [6] 0x55636d6f5c48: 1
9[1] 0x7ffd881e77d8 A.m1 = 1
10[2] 0x7ffd881e77dc B.m2 = 2
11[3] 0x7ffd881e77e0 D::C::vptr->
12 [0] 0x55636d6f5c58: D::foo()
13 [1] 0x55636d6f5c60: A::bar()
14 [2] 0x55636d6f5c68: D::quux()
15 [3] 0x55636d6f5c70: C::quuz()
16 [4] 0x55636d6f5c78: 0
17[4] 0x7ffd881e77e8 A.m1 = 1
18[5] 0x7ffd881e77ec C.m3 = 3
19[6] 0x7ffd881e77f0 D.m4 = 4
运行结果表明,重复继承的情况下:
-
有多重继承的全部特点
-
最顶部父类的成员变量会被最底层的子类继承多次
-
最顶部父类的虚函数会同时存在于多个中间父类的虚表空间
该运行结果可以用下图直观的展示出来:
由于最顶层基类的成员变量会在最底层子类的内存空间中重复出现,那么如果使用以下语句访问该成员时编译器就会报二义性错误:
1D d;
2
3/*
4main.cpp: In function ‘int main()’:
5main.cpp:75:17: error: request for member ‘m1’ is ambiguous
6*/
7std::cout << d.m1 << std::endl; //二义性错误
8
9d.B::m1 = 20;
10d.C::m1 = 30;
11std::cout << d.B::m1 << std::endl; //20
12std::cout << d.B::m2 << std::endl; //30
最后四行代码虽然消除了二义性,但是成员变量m1
实际上是2个不同变量,这种情况使用过程非常不方便且容易出错。虚拟继承便可以解决这个问题。
虚拟继承
虚拟继承的出现是为了解决重复继承出现的问题。虚拟继承的关系可以用下图来表示:
1class A {
2public:
3 A() : m1(1) {}
4 virtual void foo() { std::cout << "A::foo()" << std::endl; }
5 virtual void bar() { std::cout << "A::bar()" << std::endl; }
6 int m1;
7};
8
9class B : virtual public A {
10public:
11 B() : m2(2) {}
12 virtual void foo() { std::cout << "B::foo()" << std::endl; }
13 virtual void baz() { std::cout << "B::baz()" << std::endl; }
14 virtual void qux() { std::cout << "B::qux()" << std::endl; }
15 int m2;
16};
17
18class C : virtual public A {
19public:
20 C() : m3(3) {}
21 virtual void foo() { std::cout << "C::foo()" << std::endl; }
22 virtual void quux() { std::cout << "C::quux()" << std::endl; }
23 virtual void quuz() { std::cout << "C::quuz()" << std::endl; }
24 int m3;
25};
26
27class D : public B, public C {
28public:
29 D() : m4(4) {}
30 virtual void foo() { std::cout << "D::foo()" << std::endl; }
31 virtual void baz() { std::cout << "D::baz()" << std::endl; }
32 virtual void quux() { std::cout << "D::quux()" << std::endl; }
33 virtual void corge() { std::cout << "D::corge()" << std::endl; }
34 int m4;
35};
代码中类B
和类C
都是虚继承自类A
。我们通过测试代码来打印D
类对象的内存空间来看看发生了什么变化:
1typedef void(*Func)(void);
2
3int main() {
4 D d;
5
6 intptr_t** ptabb = reinterpret_cast<intptr_t**>(&d);
7 std::cout << "[0] " << ptabb << " D::B::vptr->" << std::endl;
8 for (int i = 0; i < 6; ++i) {
9 auto func = reinterpret_cast<Func>(*(*ptabb + i));
10 std::cout << " [" << i << "] " << *ptabb + i << ": ";
11 if (5 == i)
12 std::cout << func << std::endl;
13 else
14 func();
15 }
16
17 int* p = reinterpret_cast<int*>(reinterpret_cast<intptr_t*>(ptabb + 1));
18 std::cout << "[1] " << p + 0 << " B.m2 = " << *(p + 0) << std::endl;
19
20 intptr_t** ptabc = reinterpret_cast<intptr_t**>(ptabb + 2);
21 std::cout << "[2] " << ptabc << " D::C::vptr->" << std::endl;
22 for (int i = 0; i < 4; ++i) {
23 auto func = reinterpret_cast<Func>(*(*ptabc + i));
24 std::cout << " [" << i << "] " << *ptabc + i << ": ";
25 if (3 == i)
26 std::cout << func << std::endl;
27 else
28 func();
29 }
30
31 int* p2 = reinterpret_cast<int*>(reinterpret_cast<intptr_t*>(ptabc + 1));
32 std::cout << "[3] " << p2 + 0 << " C.m3 = " << *(p2 + 0) << std::endl;
33 std::cout << "[4] " << p2 + 1 << " D.m4 = " << *(p2 + 1) << std::endl;
34
35 intptr_t** ptaba = reinterpret_cast<intptr_t**>(ptabc + 2);
36 std::cout << "[5] " << ptaba << " D::A::vptr->" << std::endl;
37 for (int i = 0; i < 3; ++i) {
38 auto func = reinterpret_cast<Func>(*(*ptaba + i));
39 std::cout << " [" << i << "] " << *ptaba + i << ": ";
40 if (2 == i)
41 std::cout << func << std::endl;
42 else
43 func();
44 }
45
46 int* p3 = reinterpret_cast<int*>(reinterpret_cast<intptr_t*>(ptaba + 1));
47 std::cout << "[6] " << p3 + 0 << " A.m1 = " << *(p3 + 0) << std::endl;
48
49 return 0;
50}
代码的运行结果如下:
1[0] 0x7ffe87cceb10 D::B::vptr->
2 [0] 0x564b97ff2b38: D::foo()
3 [1] 0x564b97ff2b40: D::baz()
4 [2] 0x564b97ff2b48: B::qux()
5 [3] 0x564b97ff2b50: D::quux()
6 [4] 0x564b97ff2b58: D::corge()
7 [5] 0x564b97ff2b60: 1 #<-- 注意这里,非最后一个中间父类为1
8[1] 0x7ffe87cceb18 B.m2 = 2
9[2] 0x7ffe87cceb20 D::C::vptr->
10 [0] 0x564b97ff2b78: D::foo()
11 [1] 0x564b97ff2b80: D::quux()
12 [2] 0x564b97ff2b88: C::quuz()
13 [3] 0x564b97ff2b90: 0 #<-- 注意这里,最后一个中间父类为0
14[3] 0x7ffe87cceb28 C.m3 = 3
15[4] 0x7ffe87cceb2c D.m4 = 4
16[5] 0x7ffe87cceb30 D::A::vptr->
17 [0] 0x564b97ff2bb0: D::foo()
18 [1] 0x564b97ff2bb8: A::bar()
19 [2] 0x564b97ff2bc0: 1 #<-- 注意这里,虚基类永远为1
20[6] 0x7ffe87cceb38 A.m1 = 1
运行结果表明,虚拟继承的情况下:
-
虚基类的虚表指针和数据成员被挪动到最后一部分
-
最底层子类覆盖的虚函数会出现在每个一虚表空间中,未覆盖的虚函数只会出现在虚基类的虚表空间中
-
最底层子有的但中间父类没有的虚函数,会追加到第一个虚表空间中
该运行结果可以用下图直观的展示出来:
top_offset与vb_offset
前面我们提到了top_offset
指的是当前的虚表指针地址距离对象起始地址的偏移量。 如图所示,当top_offset
等于0,代表第一个虚表指针D::B::vptr
地址与对象起始地址是相等的。从测试代码和运行结果也能看出来,&d
是对象起始地址0x7ffe87cceb10
,也是首个虚表指针的地址。而vb_offset
又是什么呢?
vb_offset
指的是当前的虚表指针距离虚基类虚表指针的偏移量。 如图所示,当vb_offset
等于32,我们用当前的虚表指针地址0x7ffe87cceb10
,加上偏移量32(十六进制是0x20),结果是0x7ffe87cceb30
,这个地址刚好就是虚基类虚表指针D::A::vptr
的地址。
我们再看第二个虚表指针D::C::vptr
的地址0x7ffe87cceb20
是否也能计算出相同的虚基类虚表指针地址。如果所示,此时vb_offset
等于16(十六进制是0x10),与D::C::vptr
地址相加之后同样的到地址0x7ffe87cceb30
,即虚基类虚表指针D::A::vptr
的地址。
由此可见,虚继承时,每个非虚基类的vptr
都会计算出相同的虚基类vptr
地址,所以在构造时就不会重复构造虚基类的部分了。
top_offset
和vb_offset
两者的值之和一般为0
。如果没有虚继承,就没有vb_offset
,只有top_offset
。它们在动态绑定的过程中用来辅助快速定位各个虚表指针的地址,方便虚表指针地址和对象起始地址之间相互推导,也方便虚表指针地址和虚基类虚表指针地址之间相互推导。
thunk
我们用gcc编译器提供的功能查看一下类D
的对象内存布局:
1Vtable for D
2D::_ZTV1D: 20 entries
30 32
48 (int (*)(...))0
516 (int (*)(...))(& _ZTI1D)
624 (int (*)(...))D::foo
732 (int (*)(...))D::baz
840 (int (*)(...))B::qux
948 (int (*)(...))D::quux
1056 (int (*)(...))D::corge
1164 16
1272 (int (*)(...))-16
1380 (int (*)(...))(& _ZTI1D)
1488 (int (*)(...))D::_ZThn16_N1D3fooEv #<-- 注意这里
1596 (int (*)(...))D::_ZThn16_N1D4quuxEv
16104 (int (*)(...))C::quuz
17112 0
18120 18446744073709551584
19128 (int (*)(...))-32
20136 (int (*)(...))(& _ZTI1D)
21144 (int (*)(...))D::_ZTv0_n24_N1D3fooEv #<-- 注意这里
22152 (int (*)(...))A::bar
这是类D
的虚表结构。用c++filt
解析标注的几个符:
-
D::_ZThn16_N1D3fooEv
==>D::non-virtual thunk to D::foo()
-
D::_ZTv0_n24_N1D3fooEv
==>D::virtual thunk to D::foo()
出现了个新玩意儿thunk
,这是什么意思?
所谓thunk
是一段汇编代码,这段汇编代码可以以适当的偏移值来调整this
指针,以跳到对应的虚函数中去,并调用这个函数。例如我们的例子class D : public B, public C
:
当使用B
类指针指向D
的对象,this
不需要发生偏移;当使用C
类指针指向D
的对象,this
就需要偏移sizeof(B)
个字节,并跳转到B
中执行函数。此时通过C
类指针调用foo
函数时,其实是thunk
技术使得this
偏移到B
中,调用D::foo()
函数。
虚函数表的thunk
有以下特点:
-
如果两个父类中的虚函数名字不同,子类只覆盖了第二个父类的虚函数,则不会产生thunk用以跳转。
-
如果父类中虚函数名字相同,子类如果覆盖,将会一次性覆盖两个父类的虚函数,这时候第二个父类的虚函数表中存放的就是
thunk
对象。我们的例子属于这种情况。 -
第一个父类的虚函数被覆盖与否都不会产生
thunk
对象,因为这个类是被别的父类指针跳转的目标,而这个类的指针在进行多态的时候是不会发生跳转的。 -
子类新定义的虚函数将会追加在第一个虚函数表的后面,但是当第二个父类指针调用这个函数的时候,会通过
thunk
技术跳转回第一个类的虚函数表以执行相对应的虚函数。 -
除了第一个父类的虚析构函数,其他父类的析构函数都是
thunk
对象。