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