第十四章 继承:单继承与多继承;派生类构造与析构顺序;访问控制与继承;菱形继承与虚继承

继承,说白了就是代码复用的一种高级手段。你写了一个类,另一个类想直接用它的功能,还想加点自己的东西,那就“继承”它。C++ 的继承机制很灵活,但坑也不少。我这些年做项目,没少在继承上栽跟头,尤其是那个菱形继承,嗯,咱们慢慢聊。

14.1 单继承与多继承

单继承很好理解:一个儿子只有一个爹。多继承呢,就是一个儿子有好几个爹。C++ 支持多继承,这在 Java、C# 里是没有的。多继承用好了很强大,用不好就是灾难。

先看单继承的例子:

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

class Derived : public Base {
public:
    void derivedFunc() { cout << "Derived::derivedFunc" << endl; }
};

多继承呢,就是派生类同时继承多个基类:

class Base1 {
public:
    void func1() {}
};

class Base2 {
public:
    void func2() {}
};

class Derived : public Base1, public Base2 {
    // 现在 Derived 既有 func1 又有 func2
};
我的经验:多继承容易引发命名冲突。如果两个基类都有同名函数,派生类调用时就得加作用域限定符。我在项目中一般尽量少用多继承,除非真的需要“组合多个接口”的场景。

14.2 派生类构造与析构顺序

这个问题我面试过很多人,十个有八个答不全。构造函数的调用顺序是这样的:

  1. 先调用基类的构造函数(按继承顺序,从左到右)
  2. 再调用成员对象的构造函数(按声明顺序)
  3. 最后执行派生类自己的构造函数体

析构顺序正好相反:先析构派生类自己,再析构成员对象,最后析构基类。

看个例子就明白了:

class Base {
public:
    Base() { cout << "Base构造" << endl; }
    ~Base() { cout << "Base析构" << endl; }
};

class Member {
public:
    Member() { cout << "Member构造" << endl; }
    ~Member() { cout << "Member析构" << endl; }
};

class Derived : public Base {
private:
    Member m;
public:
    Derived() { cout << "Derived构造" << endl; }
    ~Derived() { cout << "Derived析构" << endl; }
};

输出结果:

Base构造
Member构造
Derived构造
Derived析构
Member析构
Base析构
注意:我曾经在项目中遇到一个 bug,派生类构造函数里使用了基类的某个成员,但那个成员还没初始化——因为基类构造顺序在成员对象之前,但如果你在派生类构造函数的初始化列表里调整了顺序,编译器可不听你的,它只认声明顺序。

14.3 访问控制与继承

继承方式有三种:public、protected、private。它们控制的是基类成员在派生类中的访问权限。

基类成员访问级别 public 继承 protected 继承 private 继承
public public protected private
protected protected protected private
private 不可访问 不可访问 不可访问

说白了,public 继承最常用,它保持了“is-a”关系。protected 继承和 private 继承用得少,它们更多是“实现继承”,而不是“接口继承”。

避坑指南:我曾经在代码里用 private 继承,结果子类想调用基类的 public 方法,发现调不了——因为 private 继承把基类的所有成员都变成了 private。后来我改成组合模式,代码反而更清晰。

14.4 菱形继承与虚继承

菱形继承长这样:

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

D 对象里会有两份 A 的副本。你访问 d.data 时,编译器会报歧义错误。你得用 d.B::datad.C::data 来指定。这很烦人,对吧?

解决方案就是虚继承:

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

现在 D 里只有一份 A 的副本。虚继承通过一个虚基类表(vbtable)来管理偏移,确保所有派生类共享同一个基类子对象。

我的建议:虚继承会带来额外的性能开销(多一层间接访问)。如果你不需要菱形继承,就别用虚继承。我在一个大型图形引擎里见过滥用虚继承,导致对象布局膨胀,调试起来非常痛苦。

下面我用一张图来展示菱形继承的内存布局对比:

非虚继承(两份A) A (data) A (data) B 成员 C 成员 D 成员 两份A,浪费空间 访问data需加作用域 虚继承(一份A) A (data) B 成员 C 成员 D 成员 只有一份A 通过vbtable偏移访问 无歧义,节省内存 虚继承优化
核心要点:虚继承解决了菱形继承的冗余和歧义问题,但代价是对象布局更复杂,访问虚基类成员需要间接寻址。如果你不需要多继承的菱形结构,就别用虚继承。

嗯,继承这块内容就这些。单继承简单直接,多继承灵活但需谨慎,构造析构顺序要记牢,访问控制别搞混,菱形继承用虚继承来破。我在实际项目中,一般优先用组合而不是继承,除非真的有明确的“is-a”关系。记住:继承不是万能的,组合往往更灵活。

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