25-8 虚基类

上一章第24.9节——多重继承中,我们讨论了“菱形问题”。本节将延续这一话题。

注:本节内容属于进阶主题,可根据需要跳过或略读。


菱形问题

以下是上一节中展示菱形问题的示例代码(包含部分构造函数):

#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 }
    {
    }
};

虽然你可能会期待得到一张这样的继承图:

image

若创建一个Copier类对象,默认情况下将包含两个PoweredDevice类的副本——一个来自Printer,另一个来自Scanner。其结构如下:

image

我们可以创建一个简短的示例来说明其运作原理:

int main()
{
    Copier copier{ 1, 2, 3 };

    return 0;
}

这产生了以下结果:

image

如您所见,PoweredDevice 被构造了两次。

虽然这种情况通常是期望的,但有时您可能希望仅有一个 PoweredDevice 实例被扫描仪和打印机共享。


虚拟基类

要共享基类,只需在派生类的继承列表中插入“virtual”关键字。这将创建所谓的虚拟基类,意味着仅存在一个基对象。该基对象在继承树中的所有对象间共享,且仅被构造一次。以下示例(为简化起见未包含构造函数)展示了如何使用virtual关键字创建共享基类:

class PoweredDevice
{
};

class Scanner: virtual public PoweredDevice
{
};

class Printer: virtual public PoweredDevice
{
};

class Copier: public Scanner, public Printer
{
};

现在,当你创建一个Copier类对象时,每个Copier只会获得一个PoweredDevice的副本,该副本将由扫描仪和打印机共享。

然而这又引发另一个问题:若扫描仪和打印机共享PoweredDevice基类,谁来负责创建该设备?答案是:复印机。复印机的构造函数负责创建PoweredDevice。因此,这是复印机被允许直接调用非直接父类构造函数的唯一情况:

#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 }
    {
    }
};

这次,我们之前的例子:

int main()
{
    Copier copier{ 1, 2, 3 };

    return 0;
}

产生结果:

image

如您所见,PoweredDevice 仅被构造一次。

有几点细节若不提及则有失公允。

首先,对于最派生类的构造函数,虚拟基类始终在非虚拟基类之前创建,这确保所有基类在派生类之前完成构造。

其次,请注意扫描仪和打印机的构造函数中仍包含对PoweredDevice构造函数的调用。创建复印机实例时,这些构造函数调用会被忽略,因为复印机自身负责创建PoweredDevice,而非依赖扫描仪或打印机。但若创建扫描仪或打印机实例,这些构造函数调用将被执行,此时遵循常规继承规则。

第三,若某类继承了具有虚拟基类的类,则最派生类负责构造虚拟基类。本例中,Copier继承了Printer和Scanner,二者均以PoweredDevice为虚拟基类。作为最派生类的Copier需负责创建PoweredDevice。需注意此规则在单继承场景同样成立:若Copier仅继承自Printer,而Printer通过虚继承关联PoweredDevice,Copier仍需创建PoweredDevice实例。

第四,所有继承虚基类的子类都将拥有虚函数表(即使按常规不应存在),因此该类实例的大小会增加一个指针的空间。

由于扫描仪和打印机通过虚继承从PoweredDevice派生,复印机仅包含一个PoweredDevice子对象。扫描仪和打印机都需要知道如何定位该PoweredDevice子对象,以便访问其成员(毕竟它们都派生自该基类)。这通常通过虚拟表机制实现(该表本质上存储了各子类到PoweredDevice子对象的偏移量)。

posted @ 2026-02-04 05:45  游翔  阅读(0)  评论(0)    收藏  举报