第十四章 继承:单继承与多继承;派生类构造与析构顺序;访问控制与继承;菱形继承与虚继承
继承,说白了就是代码复用的一种高级手段。你写了一个类,另一个类想直接用它的功能,还想加点自己的东西,那就“继承”它。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 派生类构造与析构顺序
这个问题我面试过很多人,十个有八个答不全。构造函数的调用顺序是这样的:
- 先调用基类的构造函数(按继承顺序,从左到右)
- 再调用成员对象的构造函数(按声明顺序)
- 最后执行派生类自己的构造函数体
析构顺序正好相反:先析构派生类自己,再析构成员对象,最后析构基类。
看个例子就明白了:
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::data 或 d.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)来管理偏移,确保所有派生类共享同一个基类子对象。
我的建议:虚继承会带来额外的性能开销(多一层间接访问)。如果你不需要菱形继承,就别用虚继承。我在一个大型图形引擎里见过滥用虚继承,导致对象布局膨胀,调试起来非常痛苦。
下面我用一张图来展示菱形继承的内存布局对比:
核心要点:虚继承解决了菱形继承的冗余和歧义问题,但代价是对象布局更复杂,访问虚基类成员需要间接寻址。如果你不需要多继承的菱形结构,就别用虚继承。
嗯,继承这块内容就这些。单继承简单直接,多继承灵活但需谨慎,构造析构顺序要记牢,访问控制别搞混,菱形继承用虚继承来破。我在实际项目中,一般优先用组合而不是继承,除非真的有明确的“is-a”关系。记住:继承不是万能的,组合往往更灵活。