17、继承:单继承、多继承、继承方式(public、protected、private)、菱形继承问题

继承,说白了就是让一个类「拥有」另一个类的特性。我刚开始学C++时,觉得这东西不就是抄代码吗?后来在Qt项目里写自定义控件,才发现继承是代码复用的灵魂。没有它,你每个按钮都得重写一遍绘制逻辑,那画面太美我不敢看。

17.1 单继承:最朴素的复用

单继承就是一个子类只继承一个父类。这是最常用的方式,也是我建议初学者优先掌握的。

class Animal {
public:
    void eat() { qDebug() << "吃东西"; }
};

class Dog : public Animal {
public:
    void bark() { qDebug() << "汪汪"; }
};

你看,Dog 类没写 eat 方法,但它能用。这就是继承的威力。我在Qt里写自定义窗口时,经常这样从 QWidget 派生,省掉大量重复代码。

我的习惯:单继承时,尽量让继承层次不超过3层。太深了,代码就像俄罗斯套娃,找bug能找到怀疑人生。

17.2 多继承:一把双刃剑

C++支持一个子类继承多个父类。听起来很强大,但用不好就是灾难。

class Teacher {
public:
    void teach() { qDebug() << "教书"; }
};

class Researcher {
public:
    void research() { qDebug() << "科研"; }
};

class Professor : public Teacher, public Researcher {
    // 同时拥有 teach 和 research 方法
};

多继承在Qt的信号槽机制里也有体现。比如 QPushButton 继承自 QAbstractButton,而 QAbstractButton 又继承自 QWidget。但Qt官方建议尽量少用多继承,我深表赞同。

我曾经踩过的坑:两个父类有同名方法时,子类调用会报歧义错误。解决办法是用作用域限定符,比如 Teacher::teach()。但说实话,这种设计本身就说明你的类结构有问题。

17.3 继承方式:public、protected、private

继承方式决定了父类成员在子类中的访问权限。我画了个表,方便你理解:

继承方式 父类public成员 父类protected成员 父类private成员
public继承 public protected 不可访问
protected继承 protected protected 不可访问
private继承 private private 不可访问

你想想看,public继承是最常用的,它表达的是「is-a」关系——子类是一种父类。protected继承和private继承用得少,但也不是没用。我在封装第三方库时,就喜欢用private继承,把不想暴露的接口藏起来。

核心原则:能用public继承就别用其他两种。protected继承适合「只让子类用,不让外部用」的场景。private继承说白了就是「实现继承」,不是「接口继承」。

17.4 菱形继承问题:C++的经典噩梦

菱形继承长这样:

class A {
public:
    int data;
};

class B : public A {};
class C : public A {};
class D : public B, public C {};

D 类里会有两份 A 的 data 成员。你调用 d.data 时,编译器会懵——你到底要 B 的那份还是 C 的那份?

我在一个图形引擎项目里遇到过这个问题。当时有个 Shape 基类,Circle 和 Rectangle 都继承它,而 RoundRect 又同时继承 Circle 和 Rectangle。结果 RoundRect 对象里有两份 Shape 的坐标数据,调试时差点把键盘砸了。

解决方案是虚继承:

class A {
public:
    int data;
};

class B : virtual public A {};
class C : virtual public A {};
class D : public B, public C {};

加上 virtual 关键字后,D 里只有一份 A 的成员。虚继承的原理是让编译器在对象布局里加一个指针,指向共享的基类部分。代价是性能稍微下降,但换来的是逻辑清晰。

注意:虚继承的构造函数调用顺序很特殊。虚基类的构造函数由最派生类直接调用,而不是由中间类调用。我建议你写代码时,尽量用组合代替继承,能避免菱形问题就别硬上。

17.5 知识体系总览

下面这张图帮你理清本章的核心脉络:

继承知识体系 继承方式 public继承 protected继承 private继承 继承类型 单继承 多继承 菱形继承 问题:数据冗余 解决:虚继承 核心原则 优先使用public单继承 多继承慎用,菱形继承用虚继承解决

17.6 避坑指南

  • 我曾经犯过的错:在public继承里把父类的析构函数写成非虚函数。结果子类对象被父类指针删除时,子类析构函数没调用,内存泄漏了。记住:只要类可能被继承,析构函数就写成虚函数。
  • 另一个教训:多继承时两个父类有同名成员变量,子类访问时编译器报错。我当时的解决办法是加作用域,但后来重构时发现,其实用组合更合适。
  • 虚继承的坑:虚基类的构造函数参数传递很麻烦。最派生类要负责初始化所有虚基类,代码维护起来像在走钢丝。我建议你非必要不用虚继承。
我的建议:写继承时,先问自己三个问题:1. 子类真的是父类的一种吗?2. 继承层次会不会太深?3. 有没有更简单的组合方案?想清楚再动手,能省掉一半的调试时间。

嗯,继承这块内容就这些。记住,继承是工具,不是目的。用得好,代码优雅复用;用不好,就是给自己挖坑。多写多练,慢慢就有感觉了。

公众号:蓝海资料掘金营,微信deep3321