参考文献
https://www.fluentcpp.com/2017/09/12/how-to-return-a-smart-pointer-and-use-covariance/
继承类型:
- 单继承
- 多继承
- 深继承
- 菱形继承
1. 菱形继承
菱形继承(Diamond Inheritance)是多重继承中的一个经典问题,当一个类通过多条路径继承同一个基类时就会出现。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Animal {
public:
int age;
virtual void speak() { std::cout << "Animal speaks\n"; }
void eat() { std::cout << "Animal eats\n"; }
};
class Mammal : public Animal {
public:
void breathe() { std::cout << "Mammal breathes\n"; }
};
class Bird : public Animal {
public:
void fly() { std::cout << "Bird flies\n"; }
};
// 问题:Bat继承了两个Animal子对象
class Bat : public Mammal, public Bird {
public:
void echolocate() { std::cout << "Bat uses echolocation\n"; }
};
菱形继承导致的问题:
- 数据成员重复:
1 2 3 4 5
Bat bat; // bat对象中包含两个Animal子对象,因此有两个age成员 // std::cout << bat.age; // 错误!编译器不知道使用哪个age std::cout << bat.Mammal::age; // 明确指定路径 std::cout << bat.Bird::age; // 明确指定路径
- 函数调用二义性:
1 2 3 4
Bat bat; // bat.speak(); // 错误!不知道调用哪个speak() bat.Mammal::speak(); // 通过Mammal路径调用 bat.Bird::speak(); // 通过Bird路径调用
- 内存浪费:
1 2
std::cout << "Bat对象大小: " << sizeof(Bat) << " bytes\n"; // 包含两个完整的Animal子对象,造成内存浪费
没有虚继承的内存布局:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Bat对象内存布局(普通继承):
+---------------------------+
| Animal部分(来自Mammal) |
| - age |
| - vptr |
+---------------------------+
| Mammal特有部分 |
+---------------------------+
| Animal部分(来自Bird) |
| - age (重复!) |
| - vptr |
+---------------------------+
| Bird特有部分 |
+---------------------------+
| Bat特有部分 |
+---------------------------+
2. 虚继承
2.1 概述
虚继承通过确保共同基类只有一个实例来解决菱形继承问题:
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
class Animal {
public:
int age = 0;
virtual void speak() { std::cout << "Animal speaks\n"; }
void eat() { std::cout << "Animal eats\n"; }
virtual ~Animal() = default;
};
class Mammal : virtual public Animal { // 虚继承
public:
void breathe() { std::cout << "Mammal breathes\n"; }
};
class Bird : virtual public Animal { // 虚继承
public:
void fly() { std::cout << "Bird flies\n"; }
};
class Bat : public Mammal, public Bird {
public:
void echolocate() { std::cout << "Bat uses echolocation\n"; }
// 现在可以直接访问Animal的成员
void setAge(int a) { age = a; } // 明确只有一个age
void makeSound() { speak(); } // 明确只有一个speak()
};
虚继承后的使用:
1
2
3
4
5
6
7
8
Bat bat;
bat.age = 5; // 正确!只有一个age
bat.speak(); // 正确!只有一个speak()
bat.breathe(); // Mammal的方法
bat.fly(); // Bird的方法
bat.echolocate(); // Bat的方法
std::cout << "优化后Bat对象大小: " << sizeof(Bat) << " bytes\n";
2.2 实现原理
虚继承通过虚基类表(Virtual Base Table)实现,类似于虚函数表。
虚继承的内存布局:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Bat对象内存布局(虚继承):
+---------------------------+
| Mammal部分 |
| - vptr (vtable) |
| - vbptr (vbtable) | <- 虚基类表指针
+---------------------------+
| Bird部分 |
| - vptr (vtable) |
| - vbptr (vbtable) | <- 虚基类表指针
+---------------------------+
| Bat特有部分 |
+---------------------------+
| 共享的Animal部分 | <- 只有一个实例
| - age |
| - vptr |
+---------------------------+
虚基类表(VBTable)示意:
1
2
3
4
5
6
7
8
9
Mammal的VBTable:
+------------------+
| Animal偏移量 | <- 指向共享Animal子对象的偏移
+------------------+
Bird的VBTable:
+------------------+
| Animal偏移量 | <- 指向同一个共享Animal子对象的偏移
+------------------+
2.3 虚继承的构造和析构
虚继承中,最派生类负责构造虚基类:
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
class Animal {
protected:
int age;
public:
Animal(int a) : age(a) {
std::cout << "Animal构造: age=" << age << std::endl;
}
virtual ~Animal() {
std::cout << "Animal析构" << std::endl;
}
};
class Mammal : virtual public Animal {
public:
Mammal(int a) : Animal(a) { // 这个调用在Bat构造时被忽略
std::cout << "Mammal构造" << std::endl;
}
~Mammal() { std::cout << "Mammal析构" << std::endl; }
};
class Bird : virtual public Animal {
public:
Bird(int a) : Animal(a) { // 这个调用在Bat构造时被忽略
std::cout << "Bird构造" << std::endl;
}
~Bird() { std::cout << "Bird析构" << std::endl; }
};
class Bat : public Mammal, public Bird {
public:
// 最派生类必须直接初始化虚基类
Bat(int a) : Animal(a), // 只有这个调用有效
Mammal(a), // Mammal中的Animal(a)被忽略
Bird(a) { // Bird中的Animal(a)被忽略
std::cout << "Bat构造" << std::endl;
}
~Bat() { std::cout << "Bat析构" << std::endl; }
};
构造顺序示例:
1
2
3
4
5
6
Bat bat(10);
// 输出:
// Animal构造: age=10 <- 虚基类最先构造
// Mammal构造 <- 第一个直接基类
// Bird构造 <- 第二个直接基类
// Bat构造 <- 最派生类
析构顺序(与构造相反):
1
2
3
4
5
// bat对象销毁时的输出:
// Bat析构
// Bird析构
// Mammal析构
// Animal析构 <- 虚基类最后析构
2.3 性能考虑
内存开销:
1
2
3
4
5
6
7
8
class Normal { int data; };
class VirtualBase { int data; };
class NormalDerived : public Normal { int more_data; };
class VirtualDerived : virtual public VirtualBase { int more_data; };
std::cout << "普通继承大小: " << sizeof(NormalDerived) << " bytes\n";
std::cout << "虚继承大小: " << sizeof(VirtualDerived) << " bytes\n";
// 虚继承通常多占用一个指针大小的空间(vbptr)
访问开销:
1
2
3
4
5
6
7
8
// 普通继承:直接偏移访问
// obj.member -> *(obj_ptr + offset)
// 虚继承:需要通过vbtable间接访问
// obj.virtual_base_member ->
// 1. 读取vbptr
// 2. 查找vbtable获取偏移
// 3. *(obj_ptr + virtual_offset)
2.4 实际应用场景
标准库中的虚继承示例:
1
2
3
4
5
6
7
// std::iostream的实现使用了虚继承
class basic_ios { /* ... */ };
class basic_istream : virtual public basic_ios { /* ... */ };
class basic_ostream : virtual public basic_ios { /* ... */ };
class basic_iostream : public basic_istream, public basic_ostream {
// 避免basic_ios的重复,使用虚继承
};
实际应用示例:
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
// 图形界面框架中的应用
class Drawable {
public:
virtual void draw() = 0;
void setVisible(bool visible) { visible_ = visible; }
protected:
bool visible_ = true;
};
class Widget : virtual public Drawable {
public:
void setSize(int w, int h) { width_ = w; height_ = h; }
protected:
int width_ = 0, height_ = 0;
};
class Clickable : virtual public Drawable {
public:
void onClick(std::function<void()> callback) { onClick_ = callback; }
protected:
std::function<void()> onClick_;
};
// Button同时需要Widget和Clickable的功能
// 但不需要两个Drawable基类
class Button : public Widget, public Clickable {
public:
void draw() override {
if (visible_) { // 只有一个visible_成员
std::cout << "Drawing button " << width_ << "x" << height_ << std::endl;
}
}
};
2.5 最佳实践
何时使用虚继承:
- 确实存在菱形继承结构
- 需要共享基类的单一实例
- 逻辑上基类只应该有一个实例
何时避免虚继承:
- 性能敏感的代码
- 简单的继承层次
- 不确定是否需要时(遵循YAGNI原则)
最佳实践:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 1. 虚基类应该尽可能简单
class Base {
public:
virtual ~Base() = default;
protected:
Base() = default; // 保护构造函数,只允许派生类构造
};
// 2. 避免在虚基类中放置数据成员(除非必要)
class DataBase {
private:
static int next_id_;
protected:
int id_;
DataBase() : id_(++next_id_) {} // 每个实例都有唯一ID
public:
int getId() const { return id_; }
virtual ~DataBase() = default;
};
// 3. 明确文档说明虚继承的使用
class InterfaceA : virtual public Base {
// 文档:使用虚继承以支持多重继承而不产生Base的重复实例
};
性能测试示例:
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
#include <chrono>
class VBase { public: int data = 42; };
class V1 : virtual public VBase {};
class V2 : virtual public VBase {};
class VDerived : public V1, public V2 {};
class NBase { public: int data = 42; };
class N1 : public NBase {};
class N2 : public NBase {};
class NDerived : public N1, public N2 {};
void performanceTest() {
const int iterations = 10000000;
VDerived vd;
NDerived nd;
auto start = std::chrono::high_resolution_clock::now();
for(int i = 0; i < iterations; ++i) {
volatile int x = vd.data; // 虚继承访问
}
auto end = std::chrono::high_resolution_clock::now();
std::cout << "虚继承访问时间: " <<
std::chrono::duration_cast<std::chrono::microseconds>(end - start).count()
<< " 微秒\n";
start = std::chrono::high_resolution_clock::now();
for(int i = 0; i < iterations; ++i) {
volatile int x = nd.N1::data; // 普通继承访问
}
end = std::chrono::high_resolution_clock::now();
std::cout << "普通继承访问时间: " <<
std::chrono::duration_cast<std::chrono::microseconds>(end - start).count()
<< " 微秒\n";
}
关键要点总结:
- 问题解决:虚继承解决菱形继承中的二义性和重复问题
- 实现原理:通过虚基类表实现,最派生类负责虚基类构造
- 性能代价:额外的指针和间接访问开销
- 使用场景:标准库iostream、GUI框架等需要避免重复基类的场景
- 设计原则:只在确实需要时使用,虚基类应该保持简单