带你读懂Effective C++(六): 继承与面向对象设计

《Effective C++》第六章 继承与面向对象设计(Inheritance and Object-Oriented Design)是全书最核心、最有深度的章节之一。它主要讨论如何在 C++ 中正确地使用继承,以及继承背后蕴含的面向对象设计原则

这一章通常包含 Item 32 ~ Item 40(第三版),一共9条条款。下面带你逐条梳理核心思想、关键结论、经典例子和最容易犯的错误,帮助你真正“读懂”这一章。

第六章整体框架

Item核心主题难度重要程度
32public 继承必须是 “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 中同名的名称,哪怕函数签名不同。
  • 解决办法
  1. 使用 using 声明把 base 的名字拉进来
    cpp class Derived : public Base { public: using Base::mf; // 让 Base::mf 可见 void mf(int); // 正常重载 };
  2. 或者使用 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 函数。其他选择:

  1. Strategy 模式(函数对象 / 函数指针 / 虚基类)
  2. 非虚接口(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 问题的多种解决方案等),或者想要看某些条款的代码示例、面试常考变形题,可以告诉我,我继续带你深入拆解。

文章已创建 4631

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

相关文章

开始在上面输入您的搜索词,然后按回车进行搜索。按ESC取消。

返回顶部