目录
[1. 复杂性急剧上升](#1. 复杂性急剧上升)
[2. 组合优于多继承](#2. 组合优于多继承)
[3. 什么时候真的需要多继承?](#3. 什么时候真的需要多继承?)
[五、完整例子:多继承 vs 组合+接口](#五、完整例子:多继承 vs 组合+接口)
[版本2:组合 + 接口(推荐)](#版本2:组合 + 接口(推荐))
[1. 以为虚拟继承是默认行为](#1. 以为虚拟继承是默认行为)
[2. 过度使用虚拟继承](#2. 过度使用虚拟继承)
[3. 忘记在最终派生类中初始化虚基类](#3. 忘记在最终派生类中初始化虚基类)
一、回顾:没有虚拟继承时的内存布局
先看非虚拟继承的菱形结构:
cpp
class Base { public: int b; };
class Base1 : public Base { public: int b1; };
class Base2 : public Base { public: int b2; };
class Derived : public Base1, public Base2 { public: int d; };
内存布局(64位,int 4字节,考虑对齐):
text
Derived对象:
┌────────────────────┐
│ Base1子对象 │
│ ├── Base子对象 │ ← b (从Base1路径来)
│ └── b1 │
├────────────────────┤
│ Base2子对象 │
│ ├── Base子对象 │ ← b (从Base2路径来,第二份)
│ └── b2 │
├────────────────────┤
│ d (Derived自己的) │
└────────────────────┘
问题:两份 Base::b,访问 d.b 有二义性。
二、虚拟继承后的内存布局
当声明虚拟继承:
cpp
class Base1 : virtual public Base { ... };
class Base2 : virtual public Base { ... };
class Derived : public Base1, public Base2 { ... };
内存布局变成:
text
Derived对象:
┌────────────────────┐
│ Base1子对象 │
│ ├── vptr_to_Base │ ← 虚基类指针(指向偏移量表)
│ └── b1 │
├────────────────────┤
│ Base2子对象 │
│ ├── vptr_to_Base │ ← 另一个虚基类指针
│ └── b2 │
├────────────────────┤
│ d (Derived自己的) │
├────────────────────┤
│ Base子对象(唯一) │ ← b (只有一份,共享)
└────────────────────┘
虚基类表(vbtable)
每个包含虚拟基类的子类都有一个隐藏的虚基类表指针(vbptr),指向一张偏移量表。当访问虚基类成员时:
cpp
derived->b = 10;
实际上被编译器转换成类似:
cpp
// 伪代码:通过vbptr找到Base子对象的偏移,再访问
char* baseAddr = (char*)derived + derived->vbptr[offset_to_Base];
*(int*)(baseAddr + offset_of_b) = 10;
这就是虚拟继承访问更慢的原因------多了一次间接寻址。
两种主流实现方式
| 编译器 | 实现方式 | 特点 |
|---|---|---|
| MSVC | 在派生类末尾放置虚基类,vbptr指向偏移量 | 布局相对简单 |
| GCC/Clang | 类似,但可能使用负偏移 | 优化空间 |
标准没有规定具体实现,但原理相通。
三、虚拟继承的构造与析构顺序
虚拟基类的构造顺序有特殊规则,比普通继承更复杂。
规则总结
-
虚拟基类 在所有非虚拟基类之前构造
-
虚拟基类按深度优先、从左到右的顺序构造
-
虚拟基类只构造一次(即使被多个路径继承)
-
析构顺序与构造顺序相反
完整示例
cpp
#include <iostream>
using namespace std;
class Grand { public: Grand() { cout << "Grand" << endl; } };
class Base1 : virtual public Grand { public: Base1() { cout << "Base1" << endl; } };
class Base2 : virtual public Grand { public: Base2() { cout << "Base2" << endl; } };
class Middle1 : public Base1 { public: Middle1() { cout << "Middle1" << endl; } };
class Middle2 : public Base2 { public: Middle2() { cout << "Middle2" << endl; } };
class Derived : public Middle1, public Middle2 { public: Derived() { cout << "Derived" << endl; } };
int main() { Derived d; }
输出:
text
Grand ← 虚拟基类最先构造(只一次)
Base1
Base2
Middle1
Middle2
Derived
关键点 :Grand 在 Base1 和 Base2 之前构造,但只构造一次。Middle1 和 Middle2 虽然是派生类,但它们被排在 Base1/Base2 之后。
四、为什么C++不推荐常规多继承?
1. 复杂性急剧上升
| 问题 | 说明 |
|---|---|
| 二义性 | 同名成员需要 Base:: 前缀 |
| 菱形继承 | 需要虚拟继承,增加复杂度 |
| 构造顺序 | 规则复杂,容易出错 |
| 向下转型 | dynamic_cast 必不可少,有开销 |
| 内存布局 | 理解困难,调试麻烦 |
2. 组合优于多继承
大多数"多继承"的场景,可以用组合 + 接口替代:
cpp
// 不推荐:多继承实现
class FlyingDog : public Dog, public Bird { ... };
// 推荐:组合 + 接口
class Flyable {
public:
virtual void fly() = 0;
virtual ~Flyable() {}
};
class FlyingDog : public Dog, public Flyable { // 只继承一个实现类 + 接口
private:
Wings wings; // 组合:用翅膀实现飞行
public:
void fly() override { wings.flap(); }
};
3. 什么时候真的需要多继承?
少数场景下多继承是合理甚至必要的:
| 场景 | 说明 | 例子 |
|---|---|---|
| 接口分离 | 继承多个纯虚接口(Java式interface) | class File : public Readable, public Writable |
| 混入类(Mixin) | 提供特定功能的小规模实现 | class LoggerMixin |
| 多重继承自ABC | 继承多个抽象基类 | 设计模式中的适配器 |
经验法则:
-
只从一个非抽象类继承(实现继承)
-
可以继承多个纯虚接口(接口继承)
-
避免从多个非抽象类继承
五、完整例子:多继承 vs 组合+接口
版本1:多继承(不推荐)
cpp
class Person {
string name;
public:
Person(string n) : name(n) {}
void eat() { cout << name << " is eating" << endl; }
};
class Employee {
int id;
public:
Employee(int i) : id(i) {}
void work() { cout << "Employee " << id << " working" << endl; }
};
// 管理者同时继承Person和Employee
class Manager : public Person, public Employee {
int level;
public:
Manager(string name, int id, int lvl)
: Person(name), Employee(id), level(lvl) {}
void manage() { cout << "Managing at level " << level << endl; }
};
问题:
-
Person和Employee如果有同名方法(如print()),出现二义性 -
两个基类各自独立,没有共同的抽象
-
如果未来
Person和Employee都继承自同一个类,会出现菱形继承
版本2:组合 + 接口(推荐)
cpp
// 接口
class Workable {
public:
virtual void work() = 0;
virtual ~Workable() {}
};
class Eatable {
public:
virtual void eat() = 0;
virtual ~Eatable() {}
};
// 独立的实现类
class PersonImpl : public Eatable {
string name;
public:
PersonImpl(string n) : name(n) {}
void eat() override { cout << name << " is eating" << endl; }
};
class EmployeeImpl : public Workable {
int id;
public:
EmployeeImpl(int i) : id(i) {}
void work() override { cout << "Employee " << id << " working" << endl; }
};
// 管理者:组合 + 实现接口
class Manager : public Workable, public Eatable {
PersonImpl person;
EmployeeImpl employee;
int level;
public:
Manager(string name, int id, int lvl)
: person(name), employee(id), level(lvl) {}
void work() override { employee.work(); }
void eat() override { person.eat(); }
void manage() { cout << "Managing at level " << level << endl; }
};
优点:
-
没有二义性问题
-
可以独立替换
PersonImpl或EmployeeImpl -
更容易测试(可以注入mock对象)
六、虚拟继承的"最终派生类"概念
在虚拟继承中,最派生类负责初始化虚基类:
cpp
class Grand {
public:
Grand(int x) { cout << "Grand: " << x << endl; }
};
class Base1 : virtual public Grand {
public:
Base1() : Grand(0) {} // 这个调用会被忽略!
};
class Base2 : virtual public Grand {
public:
Base2() : Grand(0) {} // 这个调用也会被忽略!
};
class Derived : public Base1, public Base2 {
public:
Derived() : Grand(100), Base1(), Base2() {} // 只有这里有效
};
int main() {
Derived d; // 输出 "Grand: 100",不是 0
}
规则 :在虚拟继承中,中间层的构造函数中对虚基类的调用被忽略,只有最派生类直接调用虚基类构造。
七、常见误区
1. 以为虚拟继承是默认行为
cpp
class Base1 : public Base {}; // 非虚拟
class Base2 : public Base {}; // 非虚拟
// Derived 会有两份 Base,容易出错
2. 过度使用虚拟继承
cpp
// 所有继承都是虚拟的(不必要)
class A {};
class B : virtual public A {};
class C : virtual public A {};
class D : virtual public B, virtual public C {};
虚拟继承有开销,不要滥用。
3. 忘记在最终派生类中初始化虚基类
cpp
class Derived : public Base1, public Base2 {
public:
Derived() : Base1(), Base2() {} // 忘了调用 Grand 构造
// 如果 Grand 没有默认构造,编译错误
};
八、这一篇的收获
你现在应该理解:
-
虚基类表(vbtable):存储虚基类相对于当前对象的偏移,访问虚基类需要间接寻址
-
构造顺序:虚基类最先构造,按深度优先、从左到右,且只构造一次
-
最终派生类负责:只有最派生类能初始化虚基类
-
组合优于继承:大多数多继承场景可用组合+接口替代,减少复杂性
-
合理使用场景:接口分离(Java式interface)、混入类(Mixin)
💡 小作业:定义一个
Animal(有age),Pet虚拟继承Animal,Wild虚拟继承Animal,Cat继承Pet和Wild。写代码验证Cat对象中Animal部分只有一份。尝试在Cat构造函数中初始化age,观察效果。
下一篇预告 :第20篇《override与final关键字:现代C++对继承的控制》------C++11引入了 override(检查是否正确重写)和 final(禁止继承/禁止重写)。它们让继承关系更清晰、更安全。下篇讲清楚这两个关键字的用法和最佳实践。