C++ 多重继承

C++ 多重继承(Multiple Inheritance)是指一个派生类同时继承自多个基类的特性,允许派生类组合多个基类的功能。这一特性增强了代码复用的灵活性,但也带来了复杂性(如菱形继承问题)。

基本语法

cpp 复制代码
class Base1 {
public:
    void func1() { std::cout << "Base1::func1" << std::endl; }
};

class Base2 {
public:
    void func2() { std::cout << "Base2::func2" << std::endl; }
};

// Derived 同时继承 Base1 和 Base2
class Derived : public Base1, public Base2 {
public:
    void func3() { std::cout << "Derived::func3" << std::endl; }
};

// 使用
Derived d;
d.func1(); // 来自 Base1
d.func2(); // 来自 Base2
d.func3(); // 来自 Derived

1、多重继承类型

1.1 普通多重继承

cpp 复制代码
class InputDevice {
public:
    virtual void read() = 0;
};

class OutputDevice {
public:
    virtual void write() = 0;
};

// TouchScreen 既是输入设备也是输出设备
class TouchScreen : public InputDevice, public OutputDevice {
public:
    void read() override {
        std::cout << "Reading from touch screen" << std::endl;
    }
    
    void write() override {
        std::cout << "Writing to touch screen" << std::endl;
    }
};

1.2 菱形继承

cpp 复制代码
class Person {
public:
    std::string name;
    int age;
};

class Student : public Person {
public:
    int studentId;
};

class Employee : public Person {
public:
    int employeeId;
};

// TeachingAssistant 同时继承 Student 和 Employee
class TeachingAssistant : public Student, public Employee {
public:
    std::string department;
};

在这种情况下,TeachingAssistant 对象会有两个 Person 子对象

复制代码
TeachingAssistant 对象
├── Student 部分
│   └── Person 部分 (name, age)
└── Employee 部分
    └── Person 部分 (name, age)

1.3. 虚继承

解决菱形继承问题的方法:

cpp 复制代码
class Person {
public:
    std::string name;
    int age;
};

// 使用虚继承
class Student : virtual public Person {
public:
    int studentId;
};

class Employee : virtual public Person {
public:
    int employeeId;
};

class TeachingAssistant : public Student, public Employee {
public:
    std::string department;
};

现在,TeachingAssistant 对象只有一个共享的 Person 子对象:

复制代码
TeachingAssistant 对象
├── Student 部分 (studentId)
├── Employee 部分 (employeeId)
└── Person 部分 (name, age) [共享]

1.3 虚继承的底层实现

虚继承通过 虚基类表(Virtual Base Table)虚基类指针(vbptr) 实现:

  • 每个虚继承的中间基类(如 Student、Employee)会生成一个虚基类表,存储其到顶层基类(Person)的偏移量。
  • 中间基类的对象包含一个虚基类指针(vbptr),指向自身的虚基类表。
  • 最终派生类(TeachingAssistant)初始化时,通过虚基类表找到顶层基类的唯一实例,确保只初始化一次。

核心目的:无论中间基类被继承多少次,顶层基类在最终派生类中仅保留一份,避免二义性。

2、多重继承的构造函数和析构函数

2.1 构造函数调用顺序

cpp 复制代码
class Base1 {
public:
    Base1() { std::cout << "Base1 constructor" << std::endl; }
    ~Base1() { std::cout << "Base1 destructor" << std::endl; }
};

class Base2 {
public:
    Base2() { std::cout << "Base2 constructor" << std::endl; }
    ~Base2() { std::cout << "Base2 destructor" << std::endl; }
};

class Derived : public Base1, public Base2 {
public:
    Derived() { std::cout << "Derived constructor" << std::endl; }
    ~Derived() { std::cout << "Derived destructor" << std::endl; }
};

// 输出顺序:
// Base1 constructor
// Base2 constructor
// Derived constructor
// Derived destructor
// Base2 destructor
// Base1 destructor

2.2 虚继承的构造函数

虚继承的基类由最派生的类直接初始化:

cpp 复制代码
class Person {
public:
    Person(const std::string& n, int a) : name(n), age(a) {
        std::cout << "Person constructor" << std::endl;
    }
    std::string name;
    int age;
};

class Student : virtual public Person {
public:
    Student(const std::string& n, int a, int id) 
        : Person(n, a), studentId(id) {
        std::cout << "Student constructor" << std::endl;
    }
    int studentId;
};

class Employee : virtual public Person {
public:
    Employee(const std::string& n, int a, int id) 
        : Person(n, a), employeeId(id) {
        std::cout << "Employee constructor" << std::endl;
    }
    int employeeId;
};

class TeachingAssistant : public Student, public Employee {
public:
    TeachingAssistant(const std::string& n, int a, int sid, int eid, const std::string& dept)
        : Person(n, a),          // 直接初始化虚基类
          Student(n, a, sid),    // 忽略Person初始化
          Employee(n, a, eid),   // 忽略Person初始化
          department(dept) {
        std::cout << "TeachingAssistant constructor" << std::endl;
    }
    std::string department;
};

3、名字冲突与解决方案

3.1 名字冲突与解决方案

c++ 复制代码
class Base1 {
public:
    void func() { std::cout << "Base1::func" << std::endl; }
};

class Base2 {
public:
    void func() { std::cout << "Base2::func" << std::endl; }
};

class Derived : public Base1, public Base2 {
public:
    void test() {
        // func(); // 错误:对func的调用不明确
    }
};

3.2 解决方案

使用作用域解析运算符

cpp 复制代码
class Derived : public Base1, public Base2 {
public:
    void test() {
        Base1::func(); // 明确调用Base1的func
        Base2::func(); // 明确调用Base2的func
    }
};

在派生类中重写冲突的函数

cpp 复制代码
class Derived : public Base1, public Base2 {
public:
    void func() {
        // 选择其中一个实现,或者提供新的实现
        Base1::func();
    }
};

使用using声明引入特定版本

cpp 复制代码
class Derived : public Base1, public Base2 {
public:
    using Base1::func; // 引入Base1的func
    
    void test() {
        func(); // 现在调用的是Base1::func
    }
};

4、多重继承的接口模式

4.1 纯抽象类作为接口

cpp 复制代码
// 接口类 - 只有纯虚函数
class Drawable {
public:
    virtual void draw() const = 0;
    virtual ~Drawable() = default;
};

class Clickable {
public:
    virtual void onClick() = 0;
    virtual ~Clickable() = default;
};

class Resizable {
public:
    virtual void resize(int width, int height) = 0;
    virtual ~Resizable() = default;
};

// 实现多个接口
class Button : public Drawable, public Clickable {
public:
    void draw() const override {
        std::cout << "Drawing button" << std::endl;
    }
    
    void onClick() override {
        std::cout << "Button clicked" << std::endl;
    }
};

class Window : public Drawable, public Clickable, public Resizable {
public:
    void draw() const override {
        std::cout << "Drawing window" << std::endl;
    }
    
    void onClick() override {
        std::cout << "Window clicked" << std::endl;
    }
    
    void resize(int width, int height) override {
        std::cout << "Resizing window to " << width << "x" << height << std::endl;
    }
};

4.2 混入类(Mixin Classes)

cpp 复制代码
// 混入类 - 提供特定功能
class Printable {
public:
    virtual void print() const {
        std::cout << "Printing object" << std::endl;
    }
    virtual ~Printable() = default;
};

class Loggable {
public:
    virtual void log() const {
        std::cout << "Logging object" << std::endl;
    }
    virtual ~Loggable() = default;
};

class Serializable {
public:
    virtual void serialize() const {
        std::cout << "Serializing object" << std::endl;
    }
    virtual ~Serializable() = default;
};

// 使用混入类
class Document : public Printable, public Loggable, public Serializable {
private:
    std::string content;
    
public:
    Document(const std::string& text) : content(text) {}
    
    // 可以重写混入类的方法
    void print() const override {
        std::cout << "Printing document: " << content << std::endl;
    }
};

5、替代多重继承的方案

5.1 组合

cpp 复制代码
class Engine {
public:
    void start() { std::cout << "Engine started" << std::endl; }
};

class Radio {
public:
    void play() { std::cout << "Radio playing" << std::endl; }
};

// 使用组合而不是多重继承
class Car {
private:
    Engine engine;
    Radio radio;
    
public:
    void start() { engine.start(); }
    void playMusic() { radio.play(); }
};

5.2 嵌套泛化

cpp 复制代码
class Vehicle {
public:
    virtual void move() = 0;
    virtual ~Vehicle() = default;
};

class Aircraft : public Vehicle {
public:
    void move() override { std::cout << "Flying" << std::endl; }
};

class Boat : public Vehicle {
public:
    void move() override { std::cout << "Sailing" << std::endl; }
};

// 使用嵌套而不是多重继承
class Seaplane {
private:
    Aircraft aircraft;
    Boat boat;
    
public:
    void fly() { aircraft.move(); }
    void sail() { boat.move(); }
};

6、常见问题

  1. 什么是多重继承?C++ 如何支持多重继承?

    多重继承是指一个类可以从多个基类继承特性和行为。C++ 通过在派生类声明中列出多个基类来支持多重继承,例如 class Derived : public Base1, public Base2 {};

  2. 虚继承有什么代价?

    虚继承会带来性能开销(通过额外的指针间接访问虚基类)和复杂性(虚基类由最派生类直接初始化)。

  3. 当多个基类有相同名字的成员时,会发生什么?如何解决?

    会发生名字冲突,编译器无法确定使用哪个成员。解决方法包括:

    1. 使用作用域解析运算符明确指定:Base1::memberBase2::member
    2. 在派生类中重写冲突的成员
    3. 使用 using 声明引入特定版本
  4. 多重继承中构造函数和析构函数的调用顺序是怎样的?

    • 构造函数调用顺序:

      cpp 复制代码
      class V1 { public: V1() { cout << "V1 ctor" << endl; } }; // 虚基类1
      class V2 { public: V2() { cout << "V2 ctor" << endl; } }; // 虚基类2
      class A : virtual public V1 { public: A() { cout << "A ctor" << endl; } }; // 非虚基类
      class B : virtual public V2 { public: B() { cout << "B ctor" << endl; } }; // 非虚基类
      class C : public A, public B { public: C() { cout << "C ctor" << endl; } };
      
      // 输出顺序:V1 ctor → V2 ctor → A ctor → B ctor → C ctor
      1. 先调用 虚基类 的构造函数(若有多个虚基类,按声明顺序调用)。
      2. 再调用 非虚基类 的构造函数(按派生类声明中的基类顺序调用)。
      3. 最后调用 派生类自身 的构造函数。
    • 析构函数调用顺序:与构造函数相反的顺序

  5. 虚继承的构造函数调用有什么特殊之处?

    虚基类由最派生的类直接初始化,而不是由中间基类初始化。这意味着在多重虚继承中,最派生类的构造函数需要直接调用虚基类的构造函数。

  6. 什么时候应该使用多重继承?

    1. 实现多个接口 :当类需要实现多个独立的抽象接口(仅含纯虚函数)时,多重继承是自然选择(如同时实现 PrintableSerializable)。
    2. 组合少量无冲突的功能类 :当基类之间功能独立、无同名成员且无共同基类时,多重继承风险较低(如组合 LoggerTimer 功能)。

    注意:需避免基类之间有重叠的成员或共同的祖先(防止菱形问题)。

  7. 多重继承和组合的区别?为什么优先使用组合?

  • 多重继承 :通过 class D : public A, public B 让 D 拥有 A 和 B 的功能("是一个" 关系)。
  • 组合:在 D 中定义 A 和 B 的对象作为成员,通过调用成员的方法实现功能("有一个" 关系)。

优先使用组合的原因:

  1. 避免多重继承的复杂性(如菱形问题、构造函数顺序混乱)。

  2. 降低类之间的耦合度(组合是 "松耦合",继承是 "紧耦合")。

  3. 更灵活:可动态替换成员对象(如用不同的 Flyable 实现替换鸭子的飞行方式),而继承在编译期就固定了关系。

    1. 可以用组合或嵌套泛化替代时
    2. 会导致复杂的菱形继承时
    3. 团队对多重继承的理解和经验不足时
  4. 如何处理多重继承中的异常安全?

    在多重继承中,需要特别注意构造函数的异常安全:

    1. 使用智能指针管理资源
    2. 在构造函数中避免可能抛出异常的操作
    3. 如果构造函数可能失败,考虑使用工厂函数而不是直接构造函数
    cpp 复制代码
    class ResourceHolder {
    protected:
        std::unique_ptr<Resource> resource;
        
        ResourceHolder() = default;
        
    public:
        virtual ~ResourceHolder() = default;
        
        // 禁用拷贝
        ResourceHolder(const ResourceHolder&) = delete;
        ResourceHolder& operator=(const ResourceHolder&) = delete;
    };
    
    class FileHandler : public virtual ResourceHolder {
    protected:
        FileHandler(const std::string& filename) {
            resource = std::make_unique<FileResource>(filename);
            if (!resource->isValid()) {
                throw std::runtime_error("Failed to open file");
            }
        }
    };
  5. 如何设计一个既可打印又可序列化的类?

    cpp 复制代码
    class Printable {
    public:
        virtual void print() const = 0;
        virtual ~Printable() = default;
    };
    
    class Serializable {
    public:
        virtual std::string serialize() const = 0;
        virtual void deserialize(const std::string& data) = 0;
        virtual ~Serializable() = default;
    };
    
    class Document : public Printable, public Serializable {
    private:
        std::string content;
        
    public:
        void print() const override {
            std::cout << "Document: " << content << std::endl;
        }
        
        std::string serialize() const override {
            return content;
        }
        
        void deserialize(const std::string& data) override {
            content = data;
        }
    };
  6. 多重继承的主要优点和缺点是什么?

    • 优点:更自然的建模、代码复用、接口分离
    • 缺点:增加复杂性、名字冲突、菱形继承问题、性能开销
相关推荐
博笙困了2 小时前
C++提高编程 4.0
c++
扑克中的黑桃A2 小时前
[C语言]第三章-数据类型&变量
c++
感哥3 小时前
C++ std::string
c++
感哥19 小时前
C++ 面向对象
c++
沐怡旸21 小时前
【底层机制】std::shared_ptr解决的痛点?是什么?如何实现?如何正确用?
c++·面试
感哥1 天前
C++ STL 常用算法
c++
saltymilk2 天前
C++ 模板参数推导问题小记(模板类的模板构造函数)
c++·模板元编程
感哥2 天前
C++ lambda 匿名函数
c++
沐怡旸2 天前
【底层机制】std::unique_ptr 解决的痛点?是什么?如何实现?怎么正确使用?
c++·面试