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 }
{
}
};
虽然你可能会期待得到一张这样的继承图:

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

我们可以创建一个简短的示例来说明其运作原理:
int main()
{
Copier copier{ 1, 2, 3 };
return 0;
}
这产生了以下结果:

如您所见,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;
}
产生结果:

如您所见,PoweredDevice 仅被构造一次。
有几点细节若不提及则有失公允。
首先,对于最派生类的构造函数,虚拟基类始终在非虚拟基类之前创建,这确保所有基类在派生类之前完成构造。
其次,请注意扫描仪和打印机的构造函数中仍包含对PoweredDevice构造函数的调用。创建复印机实例时,这些构造函数调用会被忽略,因为复印机自身负责创建PoweredDevice,而非依赖扫描仪或打印机。但若创建扫描仪或打印机实例,这些构造函数调用将被执行,此时遵循常规继承规则。
第三,若某类继承了具有虚拟基类的类,则最派生类负责构造虚拟基类。本例中,Copier继承了Printer和Scanner,二者均以PoweredDevice为虚拟基类。作为最派生类的Copier需负责创建PoweredDevice。需注意此规则在单继承场景同样成立:若Copier仅继承自Printer,而Printer通过虚继承关联PoweredDevice,Copier仍需创建PoweredDevice实例。
第四,所有继承虚基类的子类都将拥有虚函数表(即使按常规不应存在),因此该类实例的大小会增加一个指针的空间。
由于扫描仪和打印机通过虚继承从PoweredDevice派生,复印机仅包含一个PoweredDevice子对象。扫描仪和打印机都需要知道如何定位该PoweredDevice子对象,以便访问其成员(毕竟它们都派生自该基类)。这通常通过虚拟表机制实现(该表本质上存储了各子类到PoweredDevice子对象的偏移量)。

浙公网安备 33010602011771号