虚基类:C++中解决菱形继承问题的高级技巧

在课程“多重继承”中我们曾以“菱形继承问题”作结。本节将继续深入讨论这一话题。

注意:本节属于进阶内容,可按需跳过或略读。

一、菱形继承问题

下列示例(补充了构造函数)展示了菱形继承:

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

你或许期望继承图如下:

Code::Blocks安装

然而,默认情况下,创建 Copier 对象会得到 两份 PoweredDevice 子对象——分别来自 ScannerPrinter

Code::Blocks安装

简短示例验证:

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

输出:

PoweredDevice: 3
Scanner: 1
PoweredDevice: 3
Printer: 2

可见 PoweredDevice 被构造两次。

有时这正是所需行为;另一些场合则希望 ScannerPrinter 共享 一份 PoweredDevice

二、虚基类

要使基类共享,只需在继承列表中加入 virtual 关键字,形成所谓 虚基类。此时继承树中只有一份基类对象,且仅构造一次。下面给出简化示例:

class PoweredDevice { };

class Scanner : virtual public PoweredDevice { };

class Printer : virtual public PoweredDevice { };

class Copier : public Scanner, public Printer { };

现在创建 Copier 对象,每个 Copier 仅含一份共享的 PoweredDevice

但新问题随之产生:若 ScannerPrinter 共享 PoweredDevice,谁来负责构造?答案是 最派生类 CopierCopier 构造函数可直接调用非直接基类 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 仅构造一次。

三、关键细节补充

1. 构造顺序:对最派生类,虚基类总是先于非虚基类构造,确保所有基类在其派生类之前完成构造。

2. 子对象忽略规则:尽管 ScannerPrinter 构造函数仍包含对 PoweredDevice 的调用,但当实例化 Copier 时,这些调用被忽略——由 Copier 负责构造 PoweredDevice。若直接实例化 ScannerPrinter,则仍按普通继承规则执行构造函数。

3. 最派生类责任:若某类继承了一个或多个拥有虚父类的类,则该类(最派生类)负责构造虚基类。即使单继承亦然:若 Copier 仅继承 Printer,而 Printer 虚继承自 PoweredDeviceCopier 仍需构造 PoweredDevice

4. 虚表开销:所有继承虚基类的类都会拥有虚表(即使原本无需),因此对象大小额外增加一个指针。

5. 子对象定位:由于 ScannerPrinter 均虚继承自 PoweredDeviceCopier 仅含一份 PoweredDevice 子对象。ScannerPrinter 需知道如何找到这唯一子对象,通常通过虚表机制存储到 PoweredDevice 子对象的偏移量实现。

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"