目录

一、多继承与菱形继承

二、继承与组合

1、继承示例 (白箱复用)

继承的特点

2、组合示例 (黑箱复用)

组合的特点

三、选择继承还是组合?(原因)

1、白箱复用与黑箱复用

2、实践建议

3、关键区别总结

四、相关笔试面试题

1、什么是菱形继承?它会导致哪些问题?

2、什么是菱形虚拟继承?如何解决数据冗余和二义性?

3、继承和组合的区别?何时使用继承?何时使用组合?


一、多继承与菱形继承

        许多开发者认为 C++ 语法复杂,多继承就是其中一个典型体现。多继承引入了菱形继承的可能性,而菱形继承又进一步引发了菱形虚拟继承的需求,其底层实现机制较为复杂。因此,一般不建议设计出菱形继承结构,否则不仅会增加代码的复杂度和维护难度,还可能引发性能问题。一旦菱形继承出现问题,调试和分析将变得困难,并且通常伴随一定的效率损失。多继承常被视为 C++ 的一个设计缺陷,因此许多后续的面向对象(Object Oriented)语言,如 Java,都选择不支持多继承。


二、继承与组合

        继承表示的是 is-a 关系,即每个派生类对象都是一个基类对象;而组合表示的是一种 has-a 关系,如果类 B 组合了类 A,那么每个 B 对象中都包含一个 A 对象。

关于白箱复用和黑箱复用,下一点会详细讲解:

1、继承示例 (白箱复用)

#include <iostream>
using namespace std;

// 基类:发动机
class Engine {
public:
    void start() {
        cout << "Engine starts: Vroom vroom!" << endl;
    }
    
    // 注意:这是protected成员,对派生类可见
protected:
    string type = "V8";
};

// 派生类:汽车 (继承自发动机)
class Car : public Engine {
public:
    void drive() {
        // 可以直接访问基类的protected成员
        cout << "Car with " << type << " engine is driving" << endl;
        start(); // 可以直接调用基类方法
    }
};

int main() {
    Car myCar;
    myCar.drive();
    return 0;
}

继承的特点

  • Car 类继承了 Engine 的所有public和protected成员

  • Car 可以直接访问 Engine 的protected成员 type

  • 这是"白箱复用" - Car 知道 Engine 的内部实现细节

  • 如果 Engine 的 type 成员改变,Car 类也需要相应修改

2、组合示例 (黑箱复用)

#include <iostream>
using namespace std;

// 发动机类(只有公共接口)
class Engine {
public:
    void start() {
        cout << "Engine starts: Vroom vroom!" << endl;
    }
    
    string getType() { // 通过接口访问,而不是直接暴露成员
        return "V8";
    }
    
private:
    string type = "V8"; // 现在是private,对外不可见
};

// 汽车类(通过组合包含发动机)
class Car {
public:
    void drive() {
        cout << "Car with " << engine.getType() << " engine is driving" << endl;
        engine.start(); // 通过公共接口调用
    }
    
private:
    Engine engine; // 组合:Car有一个Engine
};

int main() {
    Car myCar;
    myCar.drive();
    return 0;
}

组合的特点

  • Car 类包含一个 Engine 对象作为成员

  • Car 只能通过 Engine 的公共接口与之交互

  • 这是"黑箱复用" - Car 不知道 Engine 的内部实现

  • Engine 的内部改变不影响 Car,只要接口保持不变


三、选择继承还是组合?(原因)

1、白箱复用与黑箱复用

  • 继承允许根据基类的实现来定义派生类的实现(使派生类能够基于基类实现进行扩展),这种复用方式常被称为白箱复用(White-box reuse)。所谓“白箱”,指的是基类的内部细节对派生类可见。继承在一定程度上破坏了封装性:基类的改变会严重影响派生类,二者之间的依赖关系较强,耦合度较高。

  • 组合是除继承外的另一种复用机制,通过组合多个对象可以实现更复杂的功能。这种方式要求被组合的对象具有良好定义的接口,被称为黑箱复用(Black-box reuse),因为对象的内部细节不可见,仅通过接口交互。类之间没有强依赖关系,耦合度低。优先采用对象组合有助于保持类的良好封装性。

2、实践建议

        在实际开发中,应优先使用组合而不是继承,因为组合耦合度低(降低类之间的耦合度),代码更易于维护和修改,组合提供更好的封装性,支持运行时动态行为改变。然而,继承仍然有其适用场景:某些 is-a 关系天然适合使用继承,并且实现多态也必须依赖继承。如果类之间的关系既适合继承又适合组合,仍应优先选择组合。只有在确实需要"is-a"的关系,并且需要多态特性时,才使用继承。

3、关键区别总结

特性 继承 组合
关系 "是一个" (is-a) "有一个" (has-a)
复用类型 白箱复用 黑箱复用
封装性 破坏封装,基类细节对派生类可见 保持封装,只能通过接口访问
耦合度 高耦合,基类改变影响派生类 低耦合,内部实现变化不影响
灵活性 编译时确定,静态 运行时可以动态更换
访问权限 可以访问protected成员 只能访问public成员

四、相关笔试面试题

1、什么是菱形继承?它会导致哪些问题?

        菱形继承是多继承的一种特殊情形:两个子类继承自同一个父类,而另一个子类同时继承这两个子类。这种结构在继承图谱中形成菱形,故得名。菱形继承会导致派生类对象中包含多份间接基类的成员,从而引发数据冗余二义性问题。

2、什么是菱形虚拟继承?如何解决数据冗余和二义性?

        菱形虚拟继承是指在菱形继承结构中,中间子类采用虚拟继承(使用 virtual 关键字)的方式继承基类。通过虚基表指针和虚基表机制,最终派生类中仅保留一份虚基类成员,使得不同路径继承的中间类能够正确访问同一份基类成员,从而解决了数据冗余和二义性问题。

3、继承和组合的区别?何时使用继承?何时使用组合?

  • 继承表示 is-a 关系,组合表示 has-a 关系。

  • 如果两个类之间是 is-a 关系,应使用继承;如果是 has-a 关系,应使用组合。

  • 若两个类既可视为 is-a 又可视为 has-a,则优先使用组合。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐