虚继承 (virtual public) 是 C++ 中解决菱形继承问题(Diamond Problem)的核心机制。它确保了在多重继承的层级结构中,如果某个类是多个派生类的共同基类(虚基类),则在最底层的派生类对象中,该基类的子对象只存在一份副本。
核心问题:菱形继承与数据冗余/歧义
classDiagram
class Base {
+int data
}
class Derived1 {
}
class Derived2 {
}
class Final {
}
Base <|-- Derived1 : public
Base <|-- Derived2 : public
Derived1 <|-- Final : public
Derived2 <|-- Final : public
- 普通继承的问题:
- 如果
Derived1和Derived2都使用普通public继承自Base。 - 那么
Final继承自Derived1和Derived2时,Final对象中将包含两份Base的子对象(一个来自Derived1链,一个来自Derived2链)。 - 数据冗余:如果
Base有成员变量(如int data;),Final对象里会有两个独立的data,占用额外空间。 - 访问歧义:当在
Final内部或通过Final对象访问Base的成员时,编译器无法确定你想访问哪一份副本(是通过Derived1继承来的还是Derived2继承来的?),导致编译错误。
- 如果
虚继承 (virtual public) 的解决方案
classDiagram
class Base {
+int data
}
class Derived1 {
}
class Derived2 {
}
class Final {
}
Base <|-- Derived1 : virtual public
Base <|-- Derived2 : virtual public
Derived1 <|-- Final : public
Derived2 <|-- Final : public
- 当
Derived1和Derived2使用 **virtual public** 继承自Base时:Base被声明为Derived1和Derived2的虚基类(Virtual Base Class)。
- 关键效果:当
Final继承自Derived1和Derived2时(无论是以public还是其他方式),在Final的对象中,只会存在一份Base的子对象。- 消除冗余:
Base的成员变量(如data)在Final对象中只有一份。 - 消除歧义:所有通过
Final或Derived1、Derived2访问的Base成员,都指向这同一个子对象,编译不再有歧义错误。
- 消除冗余:
实现原理(编译器幕后工作)
编译器通过以下两种主要机制之一(或结合)来实现虚继承:
-
虚基类指针(vbcp/vbptr):
- 每个包含虚基类的派生类对象中,编译器会添加一个或多个指向虚基类子对象的指针(通常是隐藏的)。
- 在
Final对象中:Derived1部分包含一个指向共享Base子对象的指针。Derived2部分也包含一个指向同一个共享Base子对象的指针。- 实际的
Base子对象通常放置在Final对象的尾部或其他特定位置。
-
虚基类偏移量表:
- 类似于虚函数表(vtable),编译器可能为每个类生成一个包含虚基类子对象相对于该类起始地址的偏移量的表。
- 对象中存储指向该表的指针。
- 访问虚基类成员时,通过该指针查找偏移量表,然后计算虚基类成员的实际地址。
对对象大小的影响
- 虚继承会增加对象的大小,主要因为:
- 存储虚基类指针(或多个指针)。
- 存储虚基类偏移量表指针(如果使用该机制)。
- 可能因对齐要求而产生填充字节(padding)。
- 共享
Base子对象节省的空间 VS 指针/表指针带来的开销:当Base很大且继承路径很多时,共享通常节省空间;当Base很小且继承路径少时,指针开销可能更明显。
构造函数调用顺序的更改
虚继承引入了构造顺序的特殊规则:
- 虚基类优先:在任何非虚基类的派生类构造函数执行之前,所有虚基类的构造函数必须被完全构造好。
- 从最派生类负责:最终派生类(如
Final)的构造函数直接负责调用其所有虚基类(Base)的构造函数。- 即使虚基类是通过中间类(如
Derived1、Derived2)引入的。
- 即使虚基类是通过中间类(如
- 中间类构造函数的
Base调用被忽略:- 在
Derived1或Derived2的构造函数初始化列表中,对Base构造函数的调用会被编译器忽略(因为Final已经直接调用了它)。 - 如果
Final没有显式调用Base的构造函数,编译器会尝试调用Base的默认构造函数。如果Base没有默认构造函数,会导致编译错误。
- 在
class Base {
public:
Base(int v) : data(v) {}
int data;
};
class Derived1 : virtual public Base {
public:
Derived1() : Base(10) {} // 如果Final调用了Base,这里的Base(10)会被忽略
// ...
};
class Derived2 : virtual public Base {
public:
Derived2() : Base(20) {} // 如果Final调用了Base,这里的Base(20)会被忽略
// ...
};
class Final : public Derived1, public Derived2 {
public:
Final() : Base(5) {} // Final *必须* 直接负责初始化虚基类Base
// ...
};
在 Final 对象创建时:
Base(5)被调用(构造共享的Base子对象)。Derived1()被调用(但其中的Base(10)被忽略)。Derived2()被调用(但其中的Base(20)被忽略)。Final()自身的函数体执行。
什么时候使用虚继承
- 明确需要解决菱形继承问题:当你的类设计存在或潜在存在菱形结构,并且你希望共享共同基类的状态时。
- 接口类常用:纯粹定义接口的抽象基类(纯虚函数)经常被用作虚基类,因为它们通常不包含状态(成员变量),此时使用虚继承主要是为了统一接口和避免潜在的歧义。
总结:虚继承关键点
| 特性 | 普通继承 | 虚继承 (virtual public) |
|---|---|---|
| 菱形结构结果 | 公共基类出现多个副本 | 公共基类只有一个共享副本 |
| 核心目的 | 建立 "is-a" 关系,派生类包含基类副本 | 解决菱形问题,确保公共基类共享 |
| 数据成员 | 在最终类中可能有冗余 | 在最终类中只有一份,无冗余 |
| 成员访问 | 可能导致歧义(需要作用域解析) | 无歧义,访问共享成员 |
| 对象大小 | 可能较大(多个基类副本) | 通常较小(单个共享),但需加指针开销 |
| 构造函数调用 | 由直接派生类初始化基类 | 最终派生类负责初始化所有虚基类 |
| 内存布局 | 线性或相对简单 | 更复杂,涉及虚基类指针/偏移量表 |
| 适用场景 | 一般继承,单继承 | 解决菱形继承问题,接口继承 |
虚继承是 C++ 为解决多重继承中菱形继承问题(数据冗余和成员访问歧义)而引入的机制。通过在中间派生类(如图中的 Derived1 和 Derived2)使用 virtual public 继承自公共基类(Base),可以保证在最底层的派生类(Final)对象中,该公共基类的子对象仅有一份共享副本。这会带来内存布局的变化(通常增加指向共享基类的指针),并显著改变构造函数的调用顺序(由最终派生类直接负责初始化虚基类)。虽然它能优雅解决复杂继承问题,但也会增加对象大小和运行时开销,因此应仅在确实需要解决菱形问题时使用。
3447

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



