24-9 多重继承
迄今为止,我们所展示的所有继承示例都是单继承——即每个被继承类仅有一个父类。然而,C++提供了多重继承的能力。多重继承Multiple inheritance使派生类能够从多个父类继承成员。
假设我们要编写一个管理教师群体的程序。教师既是个人,同时也是雇员(若自主执业则为自身雇主)。通过多重继承,我们可以创建继承自Person类和Employee类的Teacher类。使用多重继承时,只需像单继承那样用逗号分隔指定基类即可。

#include <string>
#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 publicly inherits Person and 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;
}

混合类
混合类(亦作“mix-in”)是一种小型类,可通过继承为其他类添加属性。其名称暗示该类旨在被混合到其他类中,而非独立实例化。
在下例中,Box、Label 和 Tooltip 类均为混合类,我们通过继承这些类来创建新的 Button 类。
// h/t to reader Waldo for this example
#include <string>
struct Point2D
{
int x{};
int y{};
};
class Box // mixin Box class
{
public:
void setTopLeft(Point2D point) { m_topLeft = point; }
void setBottomRight(Point2D point) { m_bottomRight = point; }
private:
Point2D m_topLeft{};
Point2D m_bottomRight{};
};
class Label // mixin Label class
{
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 // mixin Tooltip class
{
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 using three mixins
int main()
{
Button button{};
button.Box::setTopLeft({ 1, 1 });
button.Box::setBottomRight({ 10, 10 });
button.Label::setText("Submit");
button.Label::setFontSize(6);
button.Tooltip::setText("Submit the form to the server");
}

您可能疑惑为何我们使用显式的Box::、Label::和Tooltip::作用域解析前缀,毕竟大多数情况下这并非必要。
- Label::setText()与Tooltip::setText()具有相同的函数原型。若调用button.setText(),编译器将报错指出函数调用存在歧义。此类情况下,必须使用前缀来明确指定所需版本。
- 在无歧义情况下,使用混合类名称可作为文档标记,说明函数调用所属的混合类,从而提升代码可读性。
- 若未来添加新混合类,无歧义情况可能演变为歧义。显式前缀的使用能有效预防此类问题。
对于高级读者:
由于混合类旨在为派生类添加功能而非提供接口,因此通常不使用虚函数(详见下一章)。相反,若需定制混合类以实现特定行为,通常采用模板机制。正因如此,混合类常被模板化。
令人惊讶的是,派生类可通过自身作为模板类型参数来继承混合基类。这种继承模式被称为奇异递归模板模式 Curiously Recurring Template Pattern(简称CRTP),其实现形式如下:
// The Curiously Recurring Template Pattern (CRTP)
template <class T>
class Mixin
{
// Mixin<T> can use template type parameter T to access members of Derived
// via (static_cast<T*>(this))
};
class Derived : public Mixin<Derived>
{
};
多重继承的问题
虽然多重继承看似是单继承的简单扩展,但它引入了诸多问题,会显著增加程序的复杂性,使其维护变得极其困难。让我们看看其中一些情况。
首先,当多个基类包含同名函数时,可能会导致歧义。例如:
#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(); // Which getID() do we call?
return 0;
}

当编译c54G.getID()时,编译器会检查WirelessAdapter类是否包含名为getID()的函数。该类并不存在此函数。随后编译器会检查父类中是否存在名为getID()的函数。看出问题所在了吗?问题在于 c54G 类中实际包含两个 getID() 函数:一个继承自 USBDevice,另一个继承自 NetworkDevice。因此该函数调用存在歧义,尝试编译时会引发编译器错误。
不过存在解决方法:可显式指定要调用的版本:
int main()
{
WirelessAdapter c54G { 5442, 181742 };
std::cout << c54G.USBDevice::getID();
return 0;
}
虽然这个解决方法相当简单,但当你的类继承自四个或六个基类时,情况就会变得复杂——这些基类本身又继承自其他类。随着继承的类越多,命名冲突的可能性呈指数级增长,而每个命名冲突都需要显式解决。
其次更为严重的是菱形继承问题,笔者常称之为“死亡之钻”。当某个类同时继承自两个各自继承自同一基类的类时,便会形成菱形继承结构。
例如考虑以下类集:
class PoweredDevice
{
};
class Scanner: public PoweredDevice
{
};
class Printer: public PoweredDevice
{
};
class Copier: public Scanner, public Printer
{
};

扫描仪和打印机都是供电设备,因此它们都继承自PoweredDevice类。然而,复印机同时具备扫描仪和打印机两种功能。
这种设计会引发诸多问题,包括复印机是否应拥有一个或两个PoweredDevice实例,以及如何解决特定类型的引用歧义。虽然大多数问题可通过显式作用域解决,但为应对新增复杂性而增加的类维护开销会导致开发时间激增。下一章(第25.8节——虚拟基类)将深入探讨解决菱形继承问题的方法。
多重继承是否弊大于利?
事实证明,多数可通过多重继承解决的问题,单继承同样能应对。许多面向对象语言(如Smalltalk、PHP)甚至不支持多重继承。诸如Java和C#等相对现代的语言,虽限制普通类只能单继承,却允许接口类多重继承(后文将详述)。这些语言禁止多重继承的核心理念在于:它会过度增加语言复杂性,最终引发的问题往往多于解决的问题。
许多作者和资深程序员认为,鉴于C++多重继承可能引发的诸多隐患,应竭力避免使用。笔者对此持不同意见,因为在特定场景下多重继承仍是最佳解决方案。但必须强调,使用多重继承时需极其审慎。
有趣的是,你可能已在不知情的情况下使用过多重继承实现的类:iostream库中的std::cin和std::cout对象正是通过多重继承实现的!
最佳实践:
除非替代方案导致更复杂的实现,否则应避免使用多重继承。

浙公网安备 33010602011771号