Item32--确定你的 public 继承塑模出 is-a 关系

核心原则

Item 32 的金句:Public Inheritance means "is-a" (公有继承意味着“是一个”).

它的严格定义是:

如果类 D (Derived) 公有继承自类 B (Base),那么每一个类型为 D 的对象同时也是一个类型为 B 的对象。 任何需要 B 类型对象的地方(函数参数、指针等),如果你把 D 类型的对象传进去,程序都必须表现正常。

这在软件工程领域对应着著名的 里氏替换原则 (Liskov Substitution Principle, LSP)


经典陷阱 1:企鹅与鸟 (The Bird-Penguin Problem)

这是最容易犯错的逻辑陷阱:生活常识 vs. 编程语义

直觉逻辑:

  1. 企鹅是鸟。
  2. 鸟会飞。
  3. 所以,企鹅应该继承自鸟,并拥有“飞”的功能。

错误代码:

class Bird {
public:
    virtual void fly(); // 鸟会飞
};

class Penguin : public Bird {
    // 继承了 fly() 接口
};

void migrate(Bird& b) {
    b.fly(); // 所有的鸟都能飞?
}

Penguin p;
migrate(p); // 逻辑错误:企鹅飞起来了!或者你在运行时报错?

问题分析: 在 C++ 中,公有继承主张的是:所有对 Base 有效的事情,对 Derived 也必须有效。 如果 Bird 定义了 fly(),就是在向全世界承诺:“所有的鸟都会飞”。既然企鹅不会飞,那么企鹅就不是(C++ 语义下的)鸟

修正方案: 区分“行为”,而不是单纯区分“物种”。

class Bird {
    // Bird 的通用属性
};

class FlyingBird : public Bird {
public:
    virtual void fly();
};

class Penguin : public Bird {
    // 企鹅是鸟,但不是 FlyingBird
};

这样,如果函数参数要求 FlyingBird,你传 Penguin 进去,编译器就会直接报错(Compile-time error),这比运行时错误好得多。


经典陷阱 2:矩形与正方形 (The Rectangle-Square Problem)

这个例子更加隐蔽,经常出现在数学背景较强的程序员代码中。

直觉逻辑: 几何学上,正方形(Square)是一个特殊的矩形(Rectangle)。所以 Square 应该公有继承自 Rectangle

错误代码:

class Rectangle {
public:
    virtual void setHeight(int newH);
    virtual void setWidth(int newW);
    virtual int height() const;
    virtual int width() const;
};

void makeBigger(Rectangle& r) {
    int oldHeight = r.height();
    
    // 这里的逻辑对于矩形是完全正确的:
    // 改变宽度不应该影响高度。
    r.setWidth(r.width() + 10); 
    
    assert(r.height() == oldHeight); // 检查高度是否未变
}

class Square : public Rectangle {
    // 正方形的特性:宽必须等于高
    // 所以我们需要重写 setWidth 和 setHeight
    // 无论改哪个,都要同时修改两边
};

Square s;
s.setWidth(10); // 假设此时 h=10, w=10
makeBigger(s);  // 灾难发生!

灾难分析:

  1. makeBigger 接收一个 Rectangle 引用。
  2. 它调用 setWidth,心里默认 Rectangle 的高度不会变(这是矩形的不变性 Invariant)。
  3. 但如果你传进去的是 SquareSquare::setWidth 为了维持正方形的特性,被迫同时也修改了高度。
  4. assert 失败,程序崩溃。

结论: 虽然在几何学上正方形是矩形,但在 C++ 的公有继承语义下,正方形并不是矩形。 因为矩形允许宽和高独立变化,而正方形不允许。它们的行为契约不同。


如何判断“Is-a”是否成立?

Scott Meyers 建议我们在设计继承体系时,必须通过以下测试:

“Is-a” 关系必须满足:Derived class 必须能无条件地替代 Base class,且不破坏程序的正确性。

如果在你的设计中:

  1. 代码需要进行类型检查 (Type Checking) 才能正常工作(例如 if (dynamic_cast<Penguin*>(&bird))),说明违反了 Item 32。
  2. 派生类屏蔽了基类的某些功能(比如把基类的 fly() 在子类中定义为报错或空操作),说明这不是一个完美的 is-a 关系。

怎么判断你是对的?(灵魂三问)

写代码的时候,当你想要让 B 继承 A 时,问自己三个问题:

  1. A 的所有函数,B 都能调用吗?
  2. A 调用的结果是正常的,B 调用的结果也必须是正常的吗?
  3. 有没有任何一个场景,用 A 没问题,换成 B 就出 Bug?

如果有任何一个答案是“不对劲”,那就千万别用公有继承

其他关系 (如果不是 Is-a 怎么办?)

如果两个类很像,但又不满足完全的 is-a,你应该考虑:

  1. Has-a (有一个): 组合 (Composition)。例如,正方形“有一个”矩形作为实现细节,或者正方形和矩形都继承自一个更抽象的 Shape 类。这对应 Item 38
  2. Is-implemented-in-terms-of (根据...实现): 私有继承 (Private Inheritance)。这对应 Item 39

总结

  • Public 继承 = Is-a (是一个)。
  • 这不仅是分类学上的归属,更是行为上的承诺
  • 基类做出的所有承诺(接口行为、不变性),派生类都必须遵守。
  • 如果发现派生类在某些行为上必须违背基类的逻辑,那么请断开继承关系
posted @ 2025-12-20 21:53  belief73  阅读(3)  评论(0)    收藏  举报