《Effective C++》第六章 继承与面向对象设计(Inheritance and Object-Oriented Design)是全书最核心、最有深度的章节之一。它主要讨论如何在 C++ 中正确地使用继承,以及继承背后蕴含的面向对象设计原则。
这一章通常包含 Item 32 ~ Item 40(第三版),一共9条条款。下面带你逐条梳理核心思想、关键结论、经典例子和最容易犯的错误,帮助你真正“读懂”这一章。
第六章整体框架
| Item | 核心主题 | 难度 | 重要程度 |
|---|---|---|---|
| 32 | public 继承必须是 “is-a” 关系 | ★★★ | ★★★★★ |
| 33 | 避免遮掩(hiding)继承而来的名称 | ★★ | ★★★★ |
| 34 | 区分接口继承与实现继承 | ★★★★ | ★★★★★ |
| 35 | 考虑 virtual 函数的其他替代方案 | ★★★★ | ★★★★ |
| 36 | 绝不重定义继承而来的 non-virtual 函数 | ★★★ | ★★★★ |
| 37 | 绝不重定义继承而来的默认参数值 | ★★ | ★★★ |
| 38 | 通过复合(composition)建模 “has-a” 或 “is-implemented-in-terms-of” | ★★★ | ★★★★ |
| 39 | 审慎使用 private 继承 | ★★★★ | ★★★★ |
| 40 | 审慎使用多重继承 | ★★★★★ | ★★★★ |
逐条精华提炼(带经典例子)
Item 32: 确定你的 public 继承塑模出 is-a 关系
Make sure public inheritance models “is-a”
- 核心:public 继承意味着 “is-a” 关系。适用于 base class 的每一件事,也必须适用于 derived class。
- 经典反例:正方形 Square 不是 Rectangle 的子类(尽管数学上看起来是)
class Rectangle {
public:
virtual void setWidth(int w) { width = w; }
virtual void setHeight(int h) { height = h; }
// ...
};
class Square : public Rectangle { /* ... */ };
→ 当你对 Square 调用 setWidth(5) 后,再调用 setHeight(10),正方形不再是正方形了!
结论:违背了 Liskov 替换原则(LSP),所以 Square 不应该 public 继承 Rectangle。
- 记住:public 继承是 最强 的关系承诺。
Item 33: 避免遮掩继承而来的名称
Avoid hiding inherited names
- derived class 中的名称会遮掩(hide)base class 中同名的名称,哪怕函数签名不同。
- 解决办法:
- 使用
using声明把 base 的名字拉进来cpp class Derived : public Base { public: using Base::mf; // 让 Base::mf 可见 void mf(int); // 正常重载 }; - 或者使用 forwarding function(转发函数)
- 记住:名称遮掩是作用域行为,与 virtual 无关。
Item 34: 区分接口继承与实现继承(非常重要!)
三种继承方式对比:
| 继承形式 | 接口继承? | 实现继承? | 典型代码写法 | 使用场景 |
|---|---|---|---|---|
| 纯虚函数 | 是 | 否 | virtual void mf() = 0; | 声明接口,强制子类实现 |
| 普通虚函数 | 是 | 是 | virtual void mf(); | 可选覆盖,有默认实现 |
| non-virtual 普通函数 | 否 | 是 | void mf(); | 强制子类使用基类实现 |
| private virtual 函数 | 否 | 是 | virtual void mf();(private) | Template Method 模式变种 |
经典建议:
- 想强制子类提供实现 → 纯虚函数
- 想提供默认实现但允许覆盖 → 普通虚函数 + 提供 protected 非虚实现函数
- 想提供不可改写的实现 → non-virtual
Item 35: 考虑 virtual 函数的其他替代方案
不要一遇到“行为不同”就写 virtual 函数。其他选择:
- Strategy 模式(函数对象 / 函数指针 / 虚基类)
- 非虚接口(NVI)(Non-Virtual Interface idiom)
class GameCharacter {
public:
void healthValue() const { // non-virtual
doHealthValue(); // virtual
}
private:
virtual void doHealthValue() const; // 真正的实现入口
};
→ 好处:预处理/后处理、日志、异常安全等控制权留在基类
Item 36: 绝不重定义继承而来的 non-virtual 函数
- non-virtual 函数是静态绑定,表示“这是接口的强制约定”。
- 如果你在 derived 中重定义它,会破坏“同一个函数在不同类型上行为一致”的承诺。
永远记住:non-virtual 是实现继承,不是接口继承。
Item 37: 绝不重定义继承而来的默认参数值
- 默认参数值是静态绑定的(编译期决定)
- virtual 函数是动态绑定的(运行期决定)
virtual void func(int x = 10); // Base
void Derived::func(int x = 20) override; // 错误用法!
调用 Base* p = new Derived; p->func(); → 用的是 Base 的默认值 10!
正确做法:要么都用默认值,要么都不提供默认值。
Item 38: 通过复合(composition)建模 “has-a” 或 “is-implemented-in-terms-of”
- “is-a” → public 继承
- “has-a” → 成员变量(复合)
- “is-implemented-in-terms-of” → private 复合(实现细节)
经典例子:
class Stack {
private:
std::deque<int> impl; // is-implemented-in-terms-of
public:
void push(int x) { impl.push_back(x); }
// ...
};
Item 39: 审慎使用 private 继承
private 继承的真正含义:
- “is-implemented-in-terms-of” (实现上是基于)
- 不会发生 Derived → Base 的隐式转换
- 主要用于实现继承而不是接口继承
现代 C++ 中,private 继承的使用频率远低于复合。除非你需要:
- virtual 函数重写
- protected 成员访问
- 空基类优化(EBO)
Item 40: 审慎使用多重继承
多继承的主要问题:
- 名称冲突(菱形继承最典型)
- 虚继承(virtual inheritance)解决菱形问题,但代价高
- 复杂性急剧上升
建议:
- 尽量只用单一继承 + 接口(纯虚基类)
- 只有在必要时才用多继承(最常见场景:多接口实现,如 MI with 纯虚基类)
现代推荐:能用复合 + 接口就别用多重继承。
总结一句话口诀
public 继承 = is-a
复合(成员) = has-a / is-implemented-in-terms-of
private 继承 = 实现继承(少用)
virtual = 接口 + 可选实现
non-virtual = 强制实现约定
默认参数 = 静态绑定,千万别在子类改
多继承 = 能不用就不用
如果你想深入某一条款(比如 NVI 模式完整代码、菱形继承虚继承原理、Square-Rectangle 问题的多种解决方案等),或者想要看某些条款的代码示例、面试常考变形题,可以告诉我,我继续带你深入拆解。