迄今为止,我们展示的所有继承示例均为单一继承(single inheritance),即每个派生类只有一个直接基类。然而,C++ 还支持多重继承(multiple inheritance)。多重继承允许派生类同时从多个基类继承成员。
例如,假设我们要编写一个程序来管理教师信息。一位教师既是一个人(Person),也是一名雇员(Employee)(如果他们为自己工作,则本人即雇主)。借助多重继承,可创建一个 Teacher
类,同时从 Person
和 Employee
继承属性。使用多重继承时,只需在派生列表中以逗号分隔指定各个基类:
#include <string> // Standard string library
#include <string_view>
class Person
{
private:
std::string m_name{};
int m_age{};
public:
Person(std::string_view name, int age)
: m_name{ name }, m_age{ age }
{
}
const std::string& getName() const { return m_name; }
int getAge() const { return m_age; }
};
class Employee
{
private:
std::string m_employer{};
double m_wage{};
public:
Employee(std::string_view employer, double wage)
: m_employer{ employer }, m_wage{ wage }
{
}
const std::string& getEmployer() const { return m_employer; }
double getWage() const { return m_wage; }
};
// Teacher 公有继承 Person 和 Employee
class Teacher : public Person, public Employee
{
private:
int m_teachesGrade{};
public:
Teacher(std::string_view name, int age, std::string_view employer, double wage, int teachesGrade)
: Person{ name, age }, Employee{ employer, wage }, m_teachesGrade{ teachesGrade }
{
}
};
int main()
{
Teacher t{ "Mary", 45, "Boo", 14.3, 8 };
return 0;
}
混入(Mixins)
混入(mixin,亦写作 mix-in)是一种小型类,通常被继承以向其他类添加功能。其名称表明该类旨在被“混入”其他类,而非独立实例化。
下例中,Box
、Label
和 Tooltip
均为混入类,我们通过继承它们来构造新的 Button
类:
// 感谢读者 Waldo 提供此示例
#include <string>
struct Point2D
{
int x{};
int y{};
};
class Box // 混入类 Box
{
public:
void setTopLeft(Point2D point) { m_topLeft = point; }
void setBottomRight(Point2D point) { m_bottomRight = point; }
private:
Point2D m_topLeft{};
Point2D m_bottomRight{};
};
class Label // 混入类 Label
{
public:
void setText(const std::string_view str) { m_text = str; }
void setFontSize(int fontSize) { m_fontSize = fontSize; }
private:
std::string m_text{};
int m_fontSize{};
};
class Tooltip // 混入类 Tooltip
{
public:
void setText(const std::string_view str) { m_text = str; }
private:
std::string m_text{};
};
class Button : public Box, public Label, public Tooltip {}; // Button 使用三个混入类
你可能注意到,我们在调用时显式使用了 Box::
、Label::
、Tooltip::
作用域限定前缀,而大多数情况下这些前缀并非必需。原因如下:
Label::setText()
与Tooltip::setText()
原型相同。若直接写button.setText()
,编译器会因调用歧义而报错;此时必须用前缀指明所需版本。- 在非歧义情况下,显式使用混入类名仍可增强代码可读性,使读者清晰了解该调用属于哪个混入。
- 当前非歧义的代码未来若增加新混入,可能变得歧义;显式前缀可提前避免此类问题。
【进阶阅读】由于混入旨在为派生类添加功能,而非提供接口,混入类通常不使用虚函数(下一章讨论)。若混入需针对特定场景定制,通常使用模板。因此,混入类往往是模板化的。
令人意外的是,派生类可将自身作为模板实参继承自混入基类,这种继承方式称为奇异递归模板模式(Curiously Recurring Template Pattern,简称 CRTP),如下所示:
// 奇异递归模板模式(CRTP)
template <class T>
class Mixin
{
// Mixin<T> 可通过 static_cast<T*>(this) 访问 Derived 的成员
};
class Derived : public Mixin<Derived>
{
};
可在 此处 查看 CRTP 的简易示例。
多重继承带来的问题
尽管多重继承看似单一继承的简单扩展,实则引入了诸多复杂性,可能显著增加程序复杂度和维护难度。以下列举若干典型情形。
1. 名称歧义
当多个基类拥有同名函数时,会产生歧义。例如:
#include <iostream>
class USBDevice
{
private:
long m_id {};
public:
USBDevice(long id)
: m_id { id }
{
}
long getID() const { return m_id; }
};
class NetworkDevice
{
private:
long m_id {};
public:
NetworkDevice(long id)
: m_id { id }
{
}
long getID() const { return m_id; }
};
class WirelessAdapter : public USBDevice, public NetworkDevice
{
public:
WirelessAdapter(long usbId, long networkId)
: USBDevice { usbId }, NetworkDevice { networkId }
{
}
};
int main()
{
WirelessAdapter c54G { 5442, 181742 };
std::cout << c54G.getID(); // 调用哪一个 getID()?
return 0;
}
编译器在 c54G.getID()
处发现 WirelessAdapter
自身无 getID
,却在两个父类各找到一个,导致调用存在二义性,编译报错。
解决方法是指明所需版本:
int main()
{
WirelessAdapter c54G { 5442, 181742 };
std::cout << c54G.USBDevice::getID();
return 0;
}
尽管此方案简单,但当继承层次增至四、六级且彼此嵌套时,命名冲突呈指数增长,每个冲突都需显式消歧,维护负担急剧上升。
2. 菱形(钻石)问题
更严重的“菱形继承”或“死亡钻石”问题:
当某类从两个类继承,而这两个类又共同继承自同一基类,形成菱形结构。
class PoweredDevice { };
class Scanner : public PoweredDevice { };
class Printer : public PoweredDevice { };
class Copier : public Scanner, public Printer { };
在此结构中,会衍生出诸如 Copier
应包含一份还是两份 PoweredDevice
、如何解决某些歧义引用等问题。虽然大部分可通过显式作用域解决,但为应对额外复杂性而增加的维护工作量,会使开发时间大幅膨胀。在课程虚基类中将讨论解决此问题的手段。
多重继承是否弊大于利?
事实上,多重继承所能解决的绝大多数问题,单一继承亦可解决。许多面向对象语言(如 Smalltalk、PHP)甚至不支持多重继承。Java、C# 等现代语言限制类只能单一继承普通类,但允许多重继承接口(后续讨论)。这些语言之所以禁止多重继承,核心原因是其带来的复杂性最终往往得不偿失。
不少作者与资深程序员主张在 C++ 中彻底避免多重继承。笔者并不完全赞同,因为在某些场景下多重继承确为最佳方案。然而,应当极度审慎地使用。
有趣的是,你早已在不知不觉中用过多重继承实现的类:iostream
库中的 std::cin
与 std::cout
便是通过多重继承实现!
最佳实践
除非替代方案会导致更高复杂度,否则应避免使用多重继承。