C++对象内存模型

Share on:
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};

ABC三个类各有一个自己的成员变量,B类覆盖了父类的foo_A虚函数,新增了虚函数bar_Bbaz_BC类覆盖了父类的foo_Abar_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;而m1m2m3的地址差都是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};

ABC三个类各有一个自己的成员变量,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_offsetvb_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对象。

Reference

Prev Post: 『C++ extern关键字』
Next Post: 『C++ RTTI与反射』