在课程“多重继承”中我们曾以“菱形继承问题”作结。本节将继续深入讨论这一话题。
注意:本节属于进阶内容,可按需跳过或略读。
一、菱形继承问题
下列示例(补充了构造函数)展示了菱形继承:
#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 子对象——分别来自 Scanner 与 Printer:

简短示例验证:
int main()
{
    Copier copier{ 1, 2, 3 };
    return 0;
}
输出:
PoweredDevice: 3
Scanner: 1
PoweredDevice: 3
Printer: 2
可见 PoweredDevice 被构造两次。
有时这正是所需行为;另一些场合则希望 Scanner 与 Printer 共享 一份 PoweredDevice。
二、虚基类
要使基类共享,只需在继承列表中加入 virtual 关键字,形成所谓 虚基类。此时继承树中只有一份基类对象,且仅构造一次。下面给出简化示例:
class PoweredDevice { };
class Scanner : virtual public PoweredDevice { };
class Printer : virtual public PoweredDevice { };
class Copier : public Scanner, public Printer { };
现在创建 Copier 对象,每个 Copier 仅含一份共享的 PoweredDevice。
但新问题随之产生:若 Scanner、Printer 共享 PoweredDevice,谁来负责构造?答案是 最派生类 Copier。Copier 构造函数可直接调用非直接基类 PoweredDevice 的构造函数:
#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 对象时需写此语句,但实例化 Copier 时会被忽略
    {
        std::cout << "Scanner: " << scanner << '\n';
    }
};
class Printer : virtual public PoweredDevice // PoweredDevice 现为虚基类
{
public:
    Printer(int printer, int power)
        : PoweredDevice{ power } // 创建 Printer 对象时需写此语句,但实例化 Copier 时会被忽略
    {
        std::cout << "Printer: " << printer << '\n';
    }
};
class Copier : public Scanner, public Printer
{
public:
    Copier(int scanner, int printer, int power)
        : PoweredDevice{ power },      // 由 Copier 构造 PoweredDevice
          Scanner{ scanner, power }, Printer{ printer, power }
    {
    }
};
再次运行:
int main()
{
    Copier copier{ 1, 2, 3 };
    return 0;
}
输出:
PoweredDevice: 3
Scanner: 1
Printer: 2
PoweredDevice 仅构造一次。
三、关键细节补充
- 构造顺序:对最派生类,虚基类总是先于非虚基类构造,确保所有基类在其派生类之前完成构造。
- 子对象忽略规则:尽管 Scanner与Printer构造函数仍包含对PoweredDevice的调用,但当实例化Copier时,这些调用被忽略——由Copier负责构造PoweredDevice。若直接实例化Scanner或Printer,则仍按普通继承规则执行构造函数。
- 最派生类责任:若某类继承了一个或多个拥有虚父类的类,则该类(最派生类)负责构造虚基类。即使单继承亦然:若 Copier仅继承Printer,而Printer虚继承自PoweredDevice,Copier仍需构造PoweredDevice。
- 虚表开销:所有继承虚基类的类都会拥有虚表(即使原本无需),因此对象大小额外增加一个指针。
- 子对象定位:由于 Scanner与Printer均虚继承自PoweredDevice,Copier仅含一份PoweredDevice子对象。Scanner、Printer需知道如何找到这唯一子对象,通常通过虚表机制存储到PoweredDevice子对象的偏移量实现。
