菱形继承是什么?
菱形继承是一个派生类通过多个中间派生类继承自同一个基类的情况,从而形成一个菱形或钻石形状的继承图。最简单的例子,子类D继承了两个父类B和C,而这两个父类又继承自一个相同的基类A。
A
/ \
B C
\ /
D
菱形继承的问题?
菱形继承会导致基类多次实例化,以上图为例,D中会有两个A的实例:一个来自B,一个来自C。基类A的数据成员和方法在D中出现了多次,这会导致:
- 二义性问题:当在D中调用A中的数据成员和方法时,编译器无法确定使用来自B还是来自C的版本
- 资源浪费:类D中存在多个类A的实例,这会导致冗余的内存使用和不必要的计算开销
// 菱形继承引发二义性问题的例子
class A {
public:
void show() {}
};
class B : public A {};
class C : public A {};
class D : public B, public C {};
int main() {
D d;
d.show(); // 编译报错"D::show不明确"
return 0;
}
如何解决菱形继承问题?虚继承和虚基类
将中间派生类对共同基类的继承定义为虚继承,可以解决菱形继承问题。
// 解决菱形继承
// A被B和C虚继承,A称为虚基类
class A {
public:
void show() {}
};
class B : virtual public A {}; // 加上virtual关键字,定义为虚继承
class C : virtual public A {}; // 加上virtual关键字,定义为虚继承
class D : public B, public C {};
int main() {
D d;
d.show(); // 不报错
return 0;
}
B和C虚继承了A,A称为虚基类。这样定义后,A的所有派生类将共享一个A的实例,因此解决了菱形继承问题。
虚继承的原理?虚基类表和虚基类表指针
虚继承通过虚基类表和虚基类表指针实现。公共基类的数据不再分别存储在中间派生类(B和C)中,而是直接存储在子类D中,B和C中只存储了指向虚基类表的虚基类表指针,虚基类表中存储了公共基类相对于中间派生类的偏移量。
我们举例来比较虚继承和非虚继承下的类结构,使用Visual Studio打印Class Layout
// 非虚继承情况
class A {
public:
int a;
void show() {}
};
class B : public A { int b; };
class C : public A { int c; };
class DDD : public B, public C { int d; };
// class DDD Layout
1>class DDD size(20):
1> +---
1> 0 | +--- (base class B)
1> 0 | | +--- (base class A)
1> 0 | | | a
1> | | +---
1> 4 | | b
1> | +---
1> 8 | +--- (base class C)
1> 8 | | +--- (base class A)
1> 8 | | | a
1> | | +---
1>12 | | c
1> | +---
1>16 | d
1> +---
非虚继承情况下,DDD的实例中包含B和C的实例,B和C的实例中分别包含A,这就导致了数据冗余和二义性问题。
// 虚继承情况
class A {
public:
int a;
void show() {}
};
class B : virtual public A { int b; }; // 加上virtual关键字,定义为虚继承
class C : virtual public A { int c; }; // 加上virtual关键字,定义为虚继承
class DDD : public B, public C { int d; };
//class DDD Layout
1>class DDD size(44):
1> +---
1> 0 | +--- (base class B)
1> 0 | | {vbptr}
1> 8 | | b
1> | | <alignment member> (size=4)
1> | | <alignment member> (size=4)
1> | +---
1>16 | +--- (base class C)
1>16 | | {vbptr}
1>24 | | c
1> | | <alignment member> (size=4)
1> | | <alignment member> (size=4)
1> | +---
1>32 | d
1> | <alignment member> (size=4)
1> +---
1> +--- (virtual base A)
1>40 | a
1> +---
// 虚基类表
1>DDD::$vbtable@B@:
1> 0 | 0
1> 1 | 40 (DDDd(B+0)A) // A相对B的偏移量为40
1>DDD::$vbtable@C@:
1> 0 | 0
1> 1 | 24 (DDDd(C+0)A) // A相对C的偏移量为24
// 虚基类信息
1>vbi: class offset o.vbptr o.vbte fVtorDisp
1> A 40 0 4 0
虚继承的情况下,在DDD的实例中,父类B和C中不再存储A的实例,而是存储了一个指向虚基类表(vbtable)的虚基类表指针(vbptr)。而在虚基类表中,存储了DDD的实例中虚基类A相对B和C的偏移量。“40 (DDDd(B+0)A)”说明,DDD的实例中A的起始地址相对B的偏移量为40。而在DDD的结构中,我们能看到B的起始位置为0,加上40刚好为A的起始位置40。C也同理,16+24=40。
虚继承下的内存模型
结合上面打印出的DDD的类结构,我们可以知道虚继承下派生类的内存模型为:
- 中间派生类:虚基类表指针(vbptr,指向虚基类表) + 属性,不包含虚基类的属性
- 本类的属性
- 虚基类的属性
虚继承下构造函数的调用顺序
- 虚基类的构造函数:虚基类的构造函数调用会被推迟到最底层派生类(即直接实例化的那个类)。虚基类只会在最底层派生类构造时被构造一次,即使它被多个中间类继承
- 非虚基类的构造函数:在虚基类构造之后,按照声明顺序构造普通基类
- 成员变量构造顺序:与非虚继承情况相同,按照它们在类中声明的顺序依次构造
- 当前类(派生类)构造函数:最后构造派生类本身
#include <iostream>
class A {
public:
A() { std::cout << "Constructing A\n"; }
};
class B : virtual public A {
public:
B() { std::cout << "Constructing B\n"; }
};
class C : virtual public A {
public:
C() { std::cout << "Constructing C\n"; }
};
class D : public B, public C {
public:
D() { std::cout << "Constructing D\n"; }
};
int main() {
D d;
return 0;
}
Constructing A
Constructing B
Constructing C
Constructing D
、虚继承、虚基类&spm=1001.2101.3001.5002&articleId=141597533&d=1&t=3&u=61a417e169194903b541f3c8729b73e6)
2361

被折叠的 条评论
为什么被折叠?



