继承与组合:C++编程中的复用艺术
目录
一、多继承与菱形继承
许多开发者认为 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,则优先使用组合。
更多推荐



所有评论(0)