刘洪江的流水帐

拾起点点滴滴, 聚沙成石.

一个连咖啡都要趁热一饮而尽的男子

虚继承

| Tags: C/C++

C++里面的virtual关键字可以用虚函数声明,也可以用于虚继承。上一篇博客讲到了《虚函数》,这篇博客就讲虚继承。

首先来看为什么需要虚继承。C++里面继承关系中有个很有名的继承结构,菱形继承,如下图所示

普通继承,派生类包含了基类所有的非static成员。如果采用普通继承,在上图的iostream类中,实际上会存在两个ios基类。这样会带来很多问题,首先最简单的是空间浪费,iostream类中存在两个相同的ios类,然后是构造效率低,需要构造两个ios类。更严重的是调用基类中的函数时,存在二义性,当iostream调用ios的成员函数时,编译器无法知道是调用istream还是ostream中的ios。

C++的解决方案就是虚拟继承(Virtual Inheritance)。虚拟继承可以说成虚继承,在本文中,这两个词是等价的。 在虚拟继承下,只有一个共享的基类子对象被继承,而无论该基类在派生层次中出现多少次。共享的基类子对象被称为虚拟基类(virtual base class)。在虚拟继承下,基类子对象的复制及由此而引起的二义性都被消除了。

先看看如果没有续集继承的情况下,菱形继承会出现什么情况

普通继承的菱形继承 (inheritance.cc) download
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
// 普继承关系下的菱形继承

#include <iostream>

using namespace std;

class top{
    public:
        top(){cout<<"This is top;"<<endl;}
        void printself(){cout << "this is in top::printself" << endl;}
};

class middle1: public top{
};

class middle2: public top{
};

class bottom: public middle1, public middle2{
    public:
        bottom(){cout<<"This is bottom;"<<endl;}
};

int main(void){
    bottom bo;
    bo.printself();
}

没有使用虚继承,那么bottom类在调用printself()就存在二义性,所以在编译的时候会报下面这样的错误。

下面就是使用虚继承的例子

虚继承的菱形继承 (virtual_inheritance.cc) download
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
// 使用虚拟继承的菱形继承

#include <iostream>

using namespace std;

class top{
    public:
        top(){cout<<"This is top;"<<endl;}
        void printself(){cout << "this is in top::printself" << endl;}
};

class middle1: virtual public top{
};

class middle2: virtual public top{
};

class bottom: public middle1, public middle2{
    public:
        bottom(){cout<<"This is bottom;"<<endl;}
};

int main(void){
    bottom bo;
    bo.printself();
}

编译和运行结果如下

上面的例子中,采用了虚继承,就没有出现二义性的问题了。虚拟继承声明时,virtual关键字可以放在继承关系的前面也可以放在后面,下面两种方式是等价的。

1
2
class middle1: virtual public top
class middle1: public virtual top

虚拟基类的构造

由虚假继承引发的第一个问题是虚拟基类的构造,例如上面的例子中,构造iostream时,构造了istream和ostream两个基类,如果是虚继承关系,那么只有一个ios虚拟基类,那么谁来构造ios呢?

普通继承关系,基类由派生类构造。虚继承下,虚基类的构造由最终派生类显示调用,即iostream负责构造ios类, 中间类的构造函数将会被抑制,无法完成虚拟基类的构造。看一个虚基类的构造例子

中间类的构造函数被抑制 (constructor.cc) download
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
// 虚基类的构造, 中间类的构造函数被抑制

#include <iostream>
#include <string>

using namespace std;

class top{
    public:
        top(string name):_name(name){
            cout<<"This is top(name); name is "<< _name << endl;
        }
        top():_name("top") {
            cout<<"This is top(); name is "<< _name << endl;
        }
        void printself(){cout << "this is in top::printself, name is " << _name << endl;}
    private:
        string _name;
};

class middle1: virtual public top{
    public:
        middle1(string name):top(name){
            cout << "this is middle1, name is " << name << endl;
        };
};

class middle2: virtual public top{
    public:
        middle2(string name):top(name){;
            cout << "this is middle2, name is " << name << endl;
        }
};

class bottom: public middle1, public middle2{
    public:
        bottom():middle1("bottom1"), middle2("bottom2"){cout<<"This is bottom;"<<endl;}
};

int main(void){
    bottom bo;
    bo.printself();
}

运行结果如下

例子中,虽然bottom显示调用了middle1和middle2的构造函数,但是top的构造却不是有这两个中间类完成的, 因为top的成员name的值为“top”,实际上是由最终派生类bottom调用了top的默认构造函数top()

要想完成虚基类top的构造,必须由最终派生类调用对应的虚基类构造函数。

最终派生类调用虚基类的构造函数 (constructor1.cc) download
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
// 最终类显示调用虚基类的构造函数

#include <iostream>
#include <string>

using namespace std;

class top{
    public:
        top(string name):_name(name){
            cout<<"This is top; name is "<< _name << endl;
        }
        top() {};
        void printself(){
            cout << "this is in top::printself, name is " << _name << endl;
        }
    private:
        string _name;
};

class middle1: virtual public top{
    public:
        middle1(string name):top(name){
            cout << "this is middle1, name is " << name << endl;
        }
    protected:
        middle1(){};
};

class middle2: virtual public top{
    public:
        middle2(string name):top(name){
            cout << "this is middle2, name is " << name << endl;
        }
    protected:
        middle2(){};

};

class bottom: public middle1, public middle2{
    public:
        bottom():top("bottom"){cout<<"This is bottom;"<<endl;}
};

class bottom1: public middle1, public middle2{
    public:
        bottom1():middle1("bazinga"),middle2("bazinga"), top("bottom1"){
            cout<<"This is bottom1;"<<endl;
        }
};

int main(void){
    bottom bo;
    bo.printself();
    cout << "------" << endl;
    bottom1 bo1;
    bo1.printself();
}

运行结果如下

在上面的例子中,bottom和bottom1都显示调用了top的构造函数,但前者没有调用了中间类的默认构造函数,后者调用了构造虚基类的构造函数,但结果对于虚基类的构造,都是由最终派生类构造的。

上面是一个中间类构造函数定义方式的好例子,当middle1和middle2做为最终派生类的时候,那么使用带参数的构造函数,做为中间类时,就声明一个为protected的默认构造函数,它仅仅完成类自身的构造和非虚拟继承的基类构造,最终派生类也不需要显示地构造中间类。

构造的顺序

普通继承是按照声明顺序进行构造的,虚继承由于先要进行虚基类的构造,再进行中间类的构造,所以构造顺序是:按照声明顺序构造虚基类,再按照声明顺序构造中间类和普通基类。

先看两个虚基类构造的例子,

top_b类不采用虚继承 (constructor_seq.cc) download
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
// 虚基类的构造顺序

#include <iostream>
#include <string>

using namespace std;

class top{
    public:
        top(string name):_name(name){
            cout<<"This is top; name is "<< _name << endl;
        }
        top() {};
        void printself(){
            cout << "this is in top::printself, name is " << _name << endl;
        }
    private:
        string _name;
};

class middle1: virtual public top{
    public:
        middle1(string name):top(name){
            cout << "this is middle1, name is " << name << endl;
        }
    protected:
        middle1(){};
};

class middle2: virtual public top{
    public:
        middle2(string name):top(name){
            cout << "this is middle2, name is " << name << endl;
        }
    protected:
        middle2(){};

};

class top_b{
    public:
        top_b(){ cout<<"This is top_b"<< endl;}
};

class middle_b: public top_b{
    public:
        middle_b(){cout << "this is middle_b" << endl;}
};

class bottom: public middle1, public middle2, public middle_b{
    public:
        bottom():middle1("bazinga"),middle2("bazinga"), top("bottom1"){
            cout<<"This is bottom;"<<endl;
        }
};

int main(void){
    bottom bo;
}

输出结果

另外一个例子

top_b类采用虚继承 (constructor_seq1.cc) download
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
// 虚基类的构造顺序

#include <iostream>
#include <string>

using namespace std;

class top{
    public:
        top(string name):_name(name){
            cout<<"This is top; name is "<< _name << endl;
        }
        top() {};
        void printself(){cout << "this is in top::printself, name is " << _name << endl;}
    private:
        string _name;
};

class middle1: virtual public top{
    public:
        middle1(string name):top(name){
            cout << "this is middle1, name is " << name << endl;
        }
    protected:
        middle1(){};
};

class middle2: virtual public top{
    public:
        middle2(string name):top(name){
            cout << "this is middle2, name is " << name << endl;
        }
    protected:
        middle2(){};

};

class top_b{
    public:
        top_b(){ cout<<"This is top_b"<< endl;}
};

class middle_b: virtual public top_b{
    public:
        middle_b(){cout << "this is middle_b" << endl;}
};

class bottom: public middle1, public middle2, public middle_b{
    public:
        bottom():middle1("bazinga"),middle2("bazinga"), top("bottom1"){cout<<"This is bottom;"<<endl;}
};

int main(void){
    bottom bo;
}

输出结果

上面这个两个例子中可以看出top_b的构造顺序是不一样的。第一个例子中,做为普通基类,它放到了middle1和middle2后面构造,但在第二个例子中将它声明为了虚基类,它就放到了middle1和middle2前面构造了。

虚拟基类成员的可视性

派生类从它的基类所继承而来的成员可被分为以下三类:

  • 虚拟基类实例,它们没有被中间类改写,可以直接调用。
  • 存在一个中间类,改写了基类的成员,那么最终派生类,调用时使用的是被中间类改写了的成员。
  • 存在二个或二个以上的中间类,重载了虚基类的成员,那么最终派生类,必须重载这个成员函数。

下面这个例子分别都涉及到了上面三种情况

虚继承中成员的可见性 (members.cc) download
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
// 成员函数的调用

#include <iostream>

using namespace std;

class top{
    public:
        void printA(){cout << "this is in top::printA" << endl;}
        void printB(){cout << "this is in top::printB" << endl;}
        void printC(){cout << "this is in top::printC" << endl;}
};

class middle1: virtual public top{
    public:
        void printB(){cout << "this is in middle1::printB" << endl;}
        void printC(){cout << "this is in middle1::printC" << endl;}
};

class middle2: virtual public top{
    public:
        void printC(){cout << "this is in middle2::printC" << endl;}
};

class bottom: public middle1, public middle2{
    public:
        void printC(){cout << "this is in bottom::printC" << endl;}
};

int main(void){
    bottom bo;
    bo.printA();
    bo.printB();
    bo.printC();
}

输出结果

由例子可以看出,上面三点分别对应了printA, printB, printC三个函数。如果不在bottom中重载printC,那么编译是会报错。

虚继承的实现原理

虚继承中,是如何实现只有虚基类的,通过虚继承类的内存分布,可以一探究竟。下面所有关于虚继承内存分布的例子都是和平台相关的:

  • 64位系统
  • 操作系统: ubuntu server 12.04
  • gcc 4.6.3

首先看一个简单的只有一层虚继承关系的例子

一层虚继承 (memory_middle1.cc) download
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
// memory layout for one layer virtual inheritance.  

#include <iostream>
#include <stdio.h>

using namespace std;

#define COUT_FUNC(a, b) (cout << a << " | " << b << " | ") 
#define COUT_CNUM(a, b) (cout << a << " | " << b) 
#define COUT_ARG3(a, b, c) (COUT_FUNC(a, b) << c)
#define COMMENT(a) printf("\e[32m%s\e[0m\n", (a));

class top{
    public:
        top(long i):_i(i){}
        top(){}
        virtual void a() {cout << "this is top::a()" << endl;}
        virtual void b() {cout << "this is top::b()" << endl;}
        long _i;
};

class middle1: virtual public top{
    public:
        middle1(long j):top(100),_j(j){}
        virtual void c() {cout << "this is middle1::c()" << endl;}
        virtual void b() {cout << "this is middle1::b()" << endl;}
        long _j;
};

typedef void (*func)(void);

int main(void) {
    long* pobj;
    func pfunc;

    cout << "---memory of middle1---" << endl;

    middle1 m1(101);
    pobj = (long*)&m1;

    pfunc = (func)((long*)*((long*)(pobj[0])));
    COUT_FUNC("middle1[0]:    vtab(0)", (long*)(pobj[0])); pfunc();
    COUT_CNUM("middle1[1]:         _j", (long)pobj[1]) << endl;

    pfunc = (func)((long*)*((long*)(pobj[2])));
    COUT_FUNC("    top[0]:    vtab(0)", (long*)(pobj[2])); pfunc();
    COUT_CNUM("    top[1]:         _i", (long)pobj[3]) << endl;

    cout << endl << "---vtable of middle1---" << endl;

    // class middle1
    COUT_ARG3("middle1[0]: offset(-3)", (long*)pobj[0] - 3, (long)*((long*)pobj[0] - 3)); COMMENT("\t\t\t# offset to vbase");
    COUT_ARG3("            offset(-2)", (long*)pobj[0] - 2, (long)*((long*)pobj[0] - 2)); COMMENT("\t\t\t# offset to begin");
    COUT_ARG3("            offset(-1)", (long*)pobj[0] - 1, (long*)*((long*)pobj[0] - 1)); COMMENT("\t\t# typeinfo for middle1");

    pfunc = (func)((long*)*((long*)(pobj[0])));
    COUT_FUNC("               vtab(0)", (long*)pobj[0]); pfunc();
    pfunc = (func)((long*)*((long*)(pobj[0]) + 1));
    COUT_FUNC("               vtab(1)", (long*)pobj[0] + 1); pfunc();
    COUT_ARG3("               vtab(2)", (long*)pobj[0] + 2, *((long*)(pobj[0])+2)); COMMENT("\t\t\t# next class offset to begin");

    // class top
    COUT_ARG3("    top[0]: offset(-3)", (long*)pobj[2] - 3, (long)*((long*)pobj[2] - 3)); COMMENT("\t\t\t# offset to vbase");
    COUT_ARG3("            offset(-2)", (long*)pobj[2] - 2, (long)*((long*)pobj[2] - 2)); COMMENT("\t\t\t# offset to begin");
    COUT_ARG3("            offset(-1)", (long*)pobj[2] - 1, (long*)*((long*)pobj[2] - 1)); COMMENT("\t\t# typeinfo for middle1");

    pfunc = (func)((long*)*((long*)(pobj[2])));
    COUT_FUNC("               vtab(0)", (long*)pobj[2]); pfunc();
    COUT_ARG3("               vtab(1)", (long*)(pobj[2]) + 1, (long*)*((long*)(pobj[2])+1)); COMMENT("\t\t# virtual thunk to middle1::b()");
    COUT_ARG3("               vtab(2)", (long*)(pobj[2]) + 2, *((long*)(pobj[2])+2)); COMMENT("\t\t\t# end of vtable");

    // VTT for middle1
    COUT_ARG3("      VTT:      vtt(0)", (long*)(pobj[2]) + 3, (long*)*((long*)(pobj[2])+3)); COMMENT("\t\t# for middle1");
    COUT_ARG3("                vtt(1)", (long*)(pobj[2]) + 4, (long*)*((long*)(pobj[2])+4)); COMMENT("\t\t# for top");
}

运行结果如下图

middle1中有两个vtable,分布指向了各自的虚函数表,而且这两个虚函数表实际是放在一张表,只是分别指向表中不同的位置。vtable的起始地址之前的3个地址分布存放了与虚继承相关的信息。

  • offset(-3)存放的是从middle1对象到虚基类top的偏移。 本例中middle1到top的偏移存放在0x401620为16个字节,top到自身的偏移存放在0x401650为0。
  • offset(-2)存放的是当前这个对象到middle对象内存起始地址的偏移。 本例中middle1到自身的偏移存放在0x401628为0,top到middle起始位置的偏移0x401658为-16
  • offset(-1)存放的是middle1类的typeinfo地址,本例中0x401630, 0x401660都存放的地址0x4016e0。在下面本例memory_middle1的符号列表图中可以看出middle1类的typeinfo地址(图中的红色部分)。

以上信息也有人称为虚继承表,里面存放了虚继承的虚基类地址,在程序寻找虚基类的时候,就是从本表中获取偏移地址,然后找到虚基类的。内存中只有一个虚基类,无论有多个派生类,所有派生类到这个基类,都是通过偏移找到虚基类。

如果派生类重载了虚基类的虚函数函数,在虚基类的虚函数对应的表现中,实际存放的是一个thunk地址(下图中的绿色部分)。例如本地中的重载的 middle1::b(),在地址0x401670存放的就是virtual thunk for middle1::b()。这个thunk仅调整this 指针并跳到middle1::b(), 所以当调用top::b()时,实际上就执行了middle1::b()

在middle1的虚表结束的时候,放入了一个数值,这个数据与它的虚基类的offset(-2)存放的数字是一样的,都是表示虚基类到类对象内存的其实地址的偏移。而虚基类的虚表结束的地方,则存放的是0。

在虚表结束后,紧跟的是一张VTT表。VTT(Virtual Table Table)是一张记录虚表的表,图中黄色部分色部分标注出来的。它分布存放了middle1类所有的虚表起始地址。VTT表的地址也可以在memory_middle1的符号列表中找到(图中的黄色部分)

使用下面的这个命令可以参看符号列表

1
nm -gC memory_middle1

部分输出结果的截图

根据上面的程序分析可以画出middle1的内存结构图如下:

下面是一个菱形结构继承的例子代码,有兴趣的读者可以下载以后,按照上面的方面分析。

菱形虚继承 (memory_bottom.cc) download
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
// memory layout for diamond virtual inheritance

#include <iostream>
#include <stdio.h>

using namespace std;

#define COUT_FUNC(a, b) (cout << a << " | " << b << " | ") 
#define COUT_CNUM(a, b) (cout << a << " | " << b) 
#define COUT_ARG3(a, b, c) (COUT_FUNC(a, b) << c)
#define COMMENT(a) printf("\e[32m%s\e[0m\n", (a));

class top{
    public:
        top(long i):_i(i){}
        top(){}
        virtual void a() {cout << "this is top::a()" << endl;}
        virtual void b() {cout << "this is top::b()" << endl;}
        long _i;
};

class middle1: virtual public top{
    public:
        middle1(long j):top(100),_j(j),_l(151){}
        virtual void c() {cout << "this is middle1::c()" << endl;}
        virtual void b() {cout << "this is middle1::b()" << endl;}
        long _j;
        long _l;
};

class middle2: virtual public top{
    public:
        middle2(long k, long m):_k(k),_m(m){}
        virtual void d() {cout << "this is middle2::d()" << endl;}
        long _k;
        long _m;
};

class bottom: virtual public middle1, virtual public middle2{
    public:
        bottom():top(5936l), middle1(5937l), middle2(5938l, 5939l), _n(10000){}
        virtual void b() {cout << "this is bottom::b()" << endl;}
        virtual void e() {cout << "this is bottom::e()" << endl;}
        long _n;
};

typedef void (*func)(void);

int main(void) {
    long* pobj;
    func pfunc;

    cout << "--- member of bottom---" << endl;
    bottom bo;
    pobj = (long*) &bo;

    cout << "bo._n: " << bo._n << endl;
    cout << "   _j: " << bo._j << endl;
    cout << "   _l: " << bo._l << endl;
    cout << "   _i: " << bo._i << endl;
    cout << "   _k: " << bo._k << endl;
    cout << "   _m: " << bo._m << endl;

    cout << endl << endl << "--- memory of bottom---" << endl;

    pfunc = (func)((long*)*((long*)(pobj[0])));
    COUT_FUNC(" bottom[0]:    vtab(0)", (long*)(pobj[0])); pfunc();
    COUT_CNUM(" bottom[1]:         _n", (long)pobj[1]) << endl;

    pfunc = (func)((long*)*((long*)(pobj[2])));
    COUT_FUNC("middle1[0]:    vtab(0)", (long*)(pobj[2])); pfunc();
    COUT_CNUM("middle1[1]:         _j", (long)pobj[3]) << endl;
    COUT_CNUM("middle1[2]:         _l", (long)pobj[4]) << endl;

    pfunc = (func)((long*)*((long*)(pobj[5])));
    COUT_FUNC("    top[0]:    vtab(0)", (long*)(pobj[5])); pfunc();
    COUT_CNUM("    top[1]:         _i", (long)pobj[6]) << endl;

    pfunc = (func)((long*)*((long*)(pobj[7])));
    COUT_FUNC("middle2[0]:    vtab(0)", (long*)(pobj[7])); pfunc();
    COUT_CNUM("middle2[1]:         _k", (long)pobj[8]) << endl;
    COUT_CNUM("middle2[2]:         _m", (long)pobj[9]) << endl;

    cout << endl << endl << "--- vtable of bottom---" << endl;

    // class bottom
    COUT_ARG3(" bottom[0]: offset(-3)", (long*)pobj[0] - 3, (long)*((long*)pobj[0] - 3)); COMMENT("\t\t\t# offset to vbase");
    COUT_ARG3("            offset(-2)", (long*)pobj[0] - 2, (long)*((long*)pobj[0] - 2)); COMMENT("\t\t\t# offset to begin");
    COUT_ARG3("            offset(-1)", (long*)pobj[0] - 1, (long*)*((long*)pobj[0] - 1)); COMMENT("\t\t# typeinfo for bottom");
    pfunc = (func)((long*)*((long*)(pobj[0])));
    COUT_FUNC("               vtab(0)", (long*)(pobj[0])); pfunc();
    pfunc = (func)((long*)*((long*)(pobj[0]) + 1));
    COUT_FUNC("               vtab(1)", (long*)pobj[0] + 1); pfunc();
    COUT_ARG3("               vtab(2)", (long*)pobj[0] + 2, *((long*)(pobj[0])+2)); COMMENT("\t\t\t# next class offset to begin");

    // class middle1
    COUT_ARG3("middle1[0]: offset(-3)", (long*)pobj[2] - 3, (long)*((long*)pobj[2] - 3)); COMMENT("\t\t\t# offset to vbase");
    COUT_ARG3("            offset(-2)", (long*)pobj[2] - 2, (long)*((long*)pobj[2] - 2)); COMMENT("\t\t\t# offset to begin");
    COUT_ARG3("            offset(-1)", (long*)pobj[2] - 1, (long*)*((long*)pobj[2] - 1)); COMMENT("\t\t# typeinfo for bottom");

    pfunc = (func)((long*)*((long*)(pobj[2])));
    COUT_FUNC("               vtab(0)", (long*)pobj[2]); pfunc();
    COUT_ARG3("               vtab(1)", (long*)(pobj[2]) + 1, (long*)*((long*)(pobj[2])+1)); COMMENT("\t\t# virtual thunk to bottom::b()");
    COUT_ARG3("               vtab(2)", (long*)pobj[2] + 2, *((long*)(pobj[2])+2)); COMMENT("\t\t\t# next class offset to begin");

    // class top 
    COUT_ARG3("    top[0]: offset(-3)", (long*)pobj[5] - 3, (long)*((long*)pobj[5] - 3)); COMMENT("\t\t\t# offset to vbase");
    COUT_ARG3("            offset(-2)", (long*)pobj[5] - 2, (long)*((long*)pobj[5] - 2)); COMMENT("\t\t\t# offset to begin");
    COUT_ARG3("            offset(-1)", (long*)pobj[5] - 1, (long*)*((long*)pobj[5] - 1)); COMMENT("\t\t# typeinfo for middle1");

    pfunc = (func)((long*)*((long*)(pobj[5])));
    COUT_FUNC("               vtab(0)", (long*)pobj[5]); pfunc();
    COUT_ARG3("               vtab(1)", (long*)(pobj[5]) + 1, (long*)*((long*)(pobj[5])+1)); COMMENT("\t\t# virtual thunk to bottom::b()");
    COUT_ARG3("               vtab(2)", (long*)(pobj[5]) + 2, *((long*)(pobj[5])+2)); COMMENT("\t\t\t# end of vtable");

    // class middle2
    COUT_ARG3("middle2[0]: offset(-3)", (long*)pobj[7] - 3, (long)*((long*)pobj[7] - 3)); COMMENT("\t\t\t# offset to vbase");
    COUT_ARG3("            offset(-2)", (long*)pobj[7] - 2, (long)*((long*)pobj[7] - 2)); COMMENT("\t\t\t# offset to begin");
    COUT_ARG3("            offset(-1)", (long*)pobj[7] - 1, (long*)*((long*)pobj[7] - 1)); COMMENT("\t\t# typeinfo for bottom");

    pfunc = (func)((long*)*((long*)(pobj[7])));
    COUT_FUNC("               vtab(0)", (long*)pobj[7]); pfunc();
    COUT_ARG3("               vtab(1)", (long*)pobj[7] + 1, *((long*)(pobj[7])+1)); COMMENT("\t\t\t# end of vtable");

    // vtt
    COUT_ARG3("      VTT:      vtt(0)", (long*)(pobj[7]) + 2, (long*)*((long*)(pobj[7])+2)); COMMENT("\t\t\t# address align");
    COUT_ARG3("                vtt(1)", (long*)(pobj[7]) + 3, (long*)*((long*)(pobj[7])+3)); COMMENT("\t\t\t# address align");
    COUT_ARG3("                vtt(2)", (long*)(pobj[7]) + 4, (long*)*((long*)(pobj[7])+4)); COMMENT("\t\t# for bottom");
    COUT_ARG3("                vtt(3)", (long*)(pobj[7]) + 5, (long*)*((long*)(pobj[7])+5)); COMMENT("\t\t# for middle1");
    COUT_ARG3("                vtt(4)", (long*)(pobj[7]) + 6, (long*)*((long*)(pobj[7])+6)); COMMENT("\t\t# for top");
    COUT_ARG3("                vtt(5)", (long*)(pobj[7]) + 7, (long*)*((long*)(pobj[7])+7)); COMMENT("\t\t# for middle2");
}

这里仅仅画出内存的结构图如下

在菱形虚继承的关系下,有下面几点需要注意:

  • 在bottom类的内存中,middle2类是放在了top类后面,相对应的,在虚表和VTT表中,middle2都被放在了top类的后面。
  • 计算到虚基类的内存偏移时,计算的是当前类和虚父类的偏移,例如图中,bottom的偏移是bottom到middle1的偏移,不是到top类的偏移。而且只记录了到middle1的偏移,没有到middle2的偏移,原因应该是在声明继承关系时,middle1在middle2之前。
  • 计算到内存开始的偏移时,所有都是按照bottom的起始地址计算。所以两个偏移量不是对应的。

什么时候使用虚继承

这是否意味着,应该尽可能地以虚拟方式派生我们的基类,以便层次结构中后续的派生类可能会需要虚拟继承,是这样吗?不!我们强烈反对,那样做对性能的影响会很严重(而且增加了后续类派生的复杂性)。

那么,我们从不应该使用虚拟继承吗?不是,在实践中几乎所有成功使用虚拟继承的例子中,凡是需要虚拟继承的整个层次结构子树,如iostream 库或Panda 子树,都是由同一个人或项目设计组一次设计完成的。

一般地,除非虚拟继承为一个眼前的设计问题提供了解决方案,否则建议不要使用它。

参考

Comments