虚基类
本节阅读量:
在上一章讨论多重继承时,我们研究了“菱形继承问题”。本节将继续讨论这个问题。
注意:本节是一个高级主题,可以跳过或略读。
菱形继承问题
下面是上一课中的示例(使用一些构造函数),说明问题:
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
39
|
#include <iostream>
class PoweredDevice
{
public:
PoweredDevice(int power)
{
std::cout << "PoweredDevice: " << power << '\n';
}
};
class Scanner: public PoweredDevice
{
public:
Scanner(int scanner, int power)
: PoweredDevice{ power }
{
std::cout << "Scanner: " << scanner << '\n';
}
};
class Printer: public PoweredDevice
{
public:
Printer(int printer, int power)
: PoweredDevice{ power }
{
std::cout << "Printer: " << printer << '\n';
}
};
class Copier: public Scanner, public Printer
{
public:
Copier(int scanner, int printer, int power)
: Scanner{ scanner, power }, Printer{ printer, power }
{
}
};
|
尽管您可能希望得到如下所示的继承关系图:
期望的继承情况
但是,如果创建 Copier 类对象,默认情况下会得到 PoweredDevice 类的两个副本——一个来自 Printer,一个来自 Scanner。它具有以下结构:
实际的继承情况
我们可以创建一个简短的示例来展示这一点:
1
2
3
4
5
6
|
int main()
{
Copier copier{ 1, 2, 3 };
return 0;
}
|
这将产生以下结果:
1
2
3
4
|
PoweredDevice: 3
Scanner: 1
PoweredDevice: 3
Printer: 2
|
如您所见,PoweredDevice 被构造了两次。
虽然这通常是期望的行为,但在其他情况下,您可能只希望 Scanner 和 Printer 共享 PoweredDevice 的一个副本。
虚基类
要共享基类,只需在派生类的继承列表中加入 “virtual” 关键字。这会创建所谓的虚基类,意味着基类对象只会被继承一次。基类对象会在继承树中的相关对象之间共享,并且只构造一次。下面是一个示例(为了简单起见,没有构造函数),展示如何使用 virtual 关键字创建共享基类:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
class PoweredDevice
{
};
class Scanner: virtual public PoweredDevice
{
};
class Printer: virtual public PoweredDevice
{
};
class Copier: public Scanner, public Printer
{
};
|
现在,当您创建 Copier 类对象时,每个 Copier 只会获得 PoweredDevice 的一个副本,该副本由 Scanner 和 Printer 共享。
然而,这又带来了一个问题:如果 Scanner 和 Printer 共享 PoweredDevice 基类,谁负责创建它?答案是 Copier。Copier 构造函数负责创建 PoweredDevice。因此,Copier 可以调用一次非直接父类的构造函数:
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
39
40
|
#include <iostream>
class PoweredDevice
{
public:
PoweredDevice(int power)
{
std::cout << "PoweredDevice: " << power << '\n';
}
};
class Scanner: virtual public PoweredDevice // 注: PoweredDevice 是虚基类
{
public:
Scanner(int scanner, int power)
: PoweredDevice{ power } // 为了创建 Scanner,必须有这一行,但当前情况下会被忽略
{
std::cout << "Scanner: " << scanner << '\n';
}
};
class Printer: virtual public PoweredDevice // 注: PoweredDevice 是虚基类
{
public:
Printer(int printer, int power)
: PoweredDevice{ power } // 为了创建 Printer,必须有这一行,但当前情况下会被忽略
{
std::cout << "Printer: " << printer << '\n';
}
};
class Copier: public Scanner, public Printer
{
public:
Copier(int scanner, int printer, int power)
: PoweredDevice{ power }, // PoweredDevice 在这里被构造
Scanner{ scanner, power }, Printer{ printer, power }
{
}
};
|
这一次,再次运行前面的示例:
1
2
3
4
5
6
|
int main()
{
Copier copier{ 1, 2, 3 };
return 0;
}
|
生成如下结果:
1
2
3
|
PoweredDevice: 3
Scanner: 1
Printer: 2
|
如您所见,PoweredDevice 只构造了一次。
这里还有一些细节。
首先,对于最底层派生类的构造函数,虚基类总是在非虚基类之前创建,这确保所有基类都在其派生类之前创建。
其次,请注意,Scanner 和 Printer 构造函数仍然调用 PoweredDevice 构造函数。创建 Copier 实例时,这些构造函数调用会被忽略,因为 Copier 负责创建 PoweredDevice,而不是 Scanner 和 Printer。然而,如果我们创建 Scanner 或 Printer 的实例,则会使用这些构造函数调用,并应用常规继承规则。
第三,如果类继承了一个或多个具有虚基类的类,则最底层派生类负责构造虚基类。在这种情况下,Copier 继承 Printer 和 Scanner,而两者都具有 PoweredDevice 虚基类。Copier 是最底层派生类,负责创建 PoweredDevice。请注意,即使在单继承情况下也是如此:如果 Copier 只从 Printer 继承,而 Printer 虚继承自 PoweredDevice,则 Copier 仍然负责创建 PoweredDevice。
第四,继承虚基类的所有类通常都会具有虚函数表,因此类的实例会多出一个指针。
因为 Scanner 和 Printer 实际上都是从 PoweredDevice 派生的,而 Copier 只会构造一个 PoweredDevice 子对象。Scanner 和 Printer 都需要知道如何找到这个 PoweredDevice 子对象,以便访问其成员(毕竟它们都是从它派生的)。这通常通过虚函数表相关机制完成(本质上会存储从每个子类到 PoweredDevice 子对象的偏移量)。
25.8 对象切片(Object slicing)
下一节