C++-虚继承与虚基类

引入

当设计对象时,使用了多继承,很容易尝试命名冲突,比如在经典的菱形继承情况时:

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 A
{
protected:
int m_a;
}
class B : public A
{
protected:
int m_b;
}
class C : public A
{
protected:
int m_c;
}
class D : public B, public C
{
public:
void set_a(int a){m_a = a;}// 命名冲突
void set_b(int b){m_b = b;}// 正确
void set_c(int c){m_c = c;}// 正确
void set_d(int d){m_d = c;}// 正确
private:
int m_d;
}

当上图的代码试图访问成员变量 m_a 时,发生了错误,因为类 B 和类 C 都有 m_a(从类 A 继承而来),编译器不知道选用哪一个,产生了歧义。

由此,C++ 提出了虚继承,使得在派生类中只保留一份间接基类的成员。

虚继承

在继承方式前面加上关键字virtual就是虚继承。

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 A
{
protected:
int m_a;
}
class B : virtual public A
{
protected:
int m_b;
}
class C : virtual public A
{
protected:
int m_c;
}
class D : public B, public C
{
public:
void set_a(int a){m_a = a;}// 正确
void set_b(int b){m_b = b;}// 正确
void set_c(int c){m_c = c;}// 正确
void set_d(int d){m_d = c;}// 正确
private:
int m_d;
}

这段代码只在 D 中保留了一份成员变量 m_a,就不会尝试歧义了。

虚继承的目的是让某个类做出声明,承诺愿意共享它的基类。其中,这个被共享的基类就称为虚基类。

虚继承的特征

1
虚派生只影响从指定了虚基类的派生类中进一步派生出来的类,它不会影响派生类本身。

虚继承的构造函数

  1. C++ 规定必须由最终的派生类来初始化虚基类;
  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
class A
{
protected:
int m_a;
}
class B : public A
{
protected:
int m_b;
}
class C : public B
{
protected:
int m_c;
}
class D : public C
{
protected:
int m_d;
}
int main()
{
A obj_a;
B obj_b;
C obj_c;
D obj_d;
return 0;
}

其内存模型如下

A 是最顶层的基类,在派生类 B、C、D 的对象中,A 类子对象始终位于最前面,偏移量是固定的为 0。

虚继承的内存模型

虚继承,恰恰和普通继承相反,大部分编译器会把基类成员变量放在派生类成员变量的后面,这样随着继承层级的增加,基类成员变量的偏移就会改变,就得通过其他方案来计算偏移量。

修改上述代码,使类 B 虚继承自类 A

1
class B : virtual public A

此时的内存模型如下

不管是虚基类的直接派生类还是间接派生类,虚基类的子对象始终位于派生类对象的最后面。

再修改代码,使类 C 再虚继承自类 B

1
class C : virtual public B

此时的内存模型如下

从上图可以看出,虚继承时的派生类对象被分成了两部分:

  1. 不带阴影的一部分偏移量固定,不会随着继承层次的增加而改变,称为固定部分;
  2. 带有阴影的一部分是虚基类的子对象,偏移量会随着继承层次的增加而改变,称为共享部分。

对于共享部分,偏移会随着继承层次的增加而改变,这就需要设计一种方案,在偏移不断变化的过程中准确地计算偏移。

VC对于上述情况的解决方案

VC 引入了虚基类表,如果某个派生类有一个或多个虚基类,编译器就会在派生类对象中安插一个指针,指向虚基类表。虚基类表其实就是一个数组,数组中的元素存放的是各个虚基类的偏移。

假设 A 是 B 的虚基类,那么各对象的内存模型如下

假设 A 是 B 的虚基类,同时 B 又是 C 的虚基类,那么各对象的内存模型如下

虚继承表中保存的是所有虚基类(包括直接继承和间接继承到的)相对于当前对象的偏移,这样通过派生类指针访问虚基类的成员变量时,不管继承层次有多深,只需要一次间接转换就可以。

多继承的建议

不提倡在程序中使用多继承,只有在比较简单和不易出现二义性的情况或实在必要时才使用多继承,能用单一继承解决的问题就不要使用多继承。