Skip to content

18.8 - 虚基类

Key Takeaway

在上一章 17.9 - 多重继承一课中,我们谈论了菱形继承问题。本章我们会继续该话题。

注意:本节是高级主题,可以作为选修。

菱形继承问题

下面是我们上一课中的例子(多了一些构造函数),说明菱形继承问题:

 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 被构建了两次。

虽然这通常是期望的结果,但在有时你可能只想让 ScannerPrinter 共享 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将只获得一个由ScannerPrinter共享的PoweredDevice副本。

但是,这又导致了另一个问题:如果ScannerPrinter共享一个PoweredDevice基类,那么谁负责创建它呢?事实证明,答案是 CopierCopier 的构造函数负责创建 PoweredDevice。因此, Copier 可以直接调用非直接父类(non-immediate-parent构造函数一次:

 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 // note: PoweredDevice is now a virtual base class
{
public:
    Scanner(int scanner, int power)
        : PoweredDevice{ power } // this line is required to create Scanner objects, but ignored in this case
    {
        std::cout << "Scanner: " << scanner << '\n';
    }
};

class Printer: virtual public PoweredDevice // note: PoweredDevice is now a virtual base class
{
public:
    Printer(int printer, int power)
        : PoweredDevice{ power } // this line is required to create Printer objects, but ignored in this case
    {
        std::cout << "Printer: " << printer << '\n';
    }
};

class Copier: public Scanner, public Printer
{
public:
    Copier(int scanner, int printer, int power)
        : PoweredDevice{ power }, // PoweredDevice is constructed here
        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 只构造一次。

有几个容易忽视的细节:

  • 首先,虚基类总是在非虚基类之前创建,这确保了所有基类都在它们的派生类之前创建。
  • 其次,请注意 ScannerPrinter 构造函数仍然有对 PoweredDevice 构造函数的调用。在创建Copier的实例时,这些构造函数调用将被忽略,因为 Copier 负责创建 PoweredDevice,而不是 ScannerPrinter 。但是,如果我们要创建ScannerPrinter的实例,就会使用这些构造函数调用,并应用正常的继承规则。

第三,如果一个类继承了一个或多个具有虚父类的类,则最后被派生的类会负责构造虚基类。在本例中,Copier 继承了PrinterScanner,它们都有一个PoweredDevice虚基类。最最后被派生的类,负责PoweredDevice的创建。注意,即使在单一继承的情况下也是这样:如果CopierPrinter单独继承,而PrinterPoweredDevice虚继承,那么Copier仍然负责创建PoweredDevice

第四,所有继承虚基类的类都将有一个虚表,即使它们通常没有虚表,因此类的实例其大小会增加一个指针。

因为ScannerPrinter实际上是从PoweredDevice派生的,所以Copier只是一个PoweredDevice子对象(subobject)。ScannerPrinter都需要知道如何找到单个PoweredDevice子对象,这样它们才能访问它的成员(因为它们毕竟是从它派生的)。这通常是通过一些虚表操作来完成的(它实际上存储了从每个子类到PoweredDevice子对象的偏移量)。