24-9 多重继承

迄今为止,我们所展示的所有继承示例都是单继承——即每个被继承类仅有一个父类。然而,C++提供了多重继承的能力。多重继承Multiple inheritance使派生类能够从多个父类继承成员。

假设我们要编写一个管理教师群体的程序。教师既是个人,同时也是雇员(若自主执业则为自身雇主)。通过多重继承,我们可以创建继承自Person类和Employee类的Teacher类。使用多重继承时,只需像单继承那样用逗号分隔指定基类即可。

image

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

image


混合类

混合类(亦作“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");
}

image

您可能疑惑为何我们使用显式的Box::、Label::和Tooltip::作用域解析前缀,毕竟大多数情况下这并非必要。

  1. Label::setText()与Tooltip::setText()具有相同的函数原型。若调用button.setText(),编译器将报错指出函数调用存在歧义。此类情况下,必须使用前缀来明确指定所需版本。
  2. 在无歧义情况下,使用混合类名称可作为文档标记,说明函数调用所属的混合类,从而提升代码可读性。
  3. 若未来添加新混合类,无歧义情况可能演变为歧义。显式前缀的使用能有效预防此类问题。
--- block start ---

对于高级读者:

由于混合类旨在为派生类添加功能而非提供接口,因此通常不使用虚函数(详见下一章)。相反,若需定制混合类以实现特定行为,通常采用模板机制。正因如此,混合类常被模板化。

令人惊讶的是,派生类可通过自身作为模板类型参数来继承混合基类。这种继承模式被称为奇异递归模板模式 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>
{
};
--- block end ---

多重继承的问题

虽然多重继承看似是单继承的简单扩展,但它引入了诸多问题,会显著增加程序的复杂性,使其维护变得极其困难。让我们看看其中一些情况。

首先,当多个基类包含同名函数时,可能会导致歧义。例如:

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

image

当编译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
{
};

image

扫描仪和打印机都是供电设备,因此它们都继承自PoweredDevice类。然而,复印机同时具备扫描仪和打印机两种功能。

这种设计会引发诸多问题,包括复印机是否应拥有一个或两个PoweredDevice实例,以及如何解决特定类型的引用歧义。虽然大多数问题可通过显式作用域解决,但为应对新增复杂性而增加的类维护开销会导致开发时间激增。下一章(第25.8节——虚拟基类)将深入探讨解决菱形继承问题的方法。


多重继承是否弊大于利?

事实证明,多数可通过多重继承解决的问题,单继承同样能应对。许多面向对象语言(如Smalltalk、PHP)甚至不支持多重继承。诸如Java和C#等相对现代的语言,虽限制普通类只能单继承,却允许接口类多重继承(后文将详述)。这些语言禁止多重继承的核心理念在于:它会过度增加语言复杂性,最终引发的问题往往多于解决的问题。

许多作者和资深程序员认为,鉴于C++多重继承可能引发的诸多隐患,应竭力避免使用。笔者对此持不同意见,因为在特定场景下多重继承仍是最佳解决方案。但必须强调,使用多重继承时需极其审慎。

有趣的是,你可能已在不知情的情况下使用过多重继承实现的类:iostream库中的std::cin和std::cout对象正是通过多重继承实现的!

最佳实践:
除非替代方案导致更复杂的实现,否则应避免使用多重继承。

posted @ 2026-02-01 08:05  游翔  阅读(1)  评论(0)    收藏  举报