Effective C++ 条款32:确定你的public继承塑模出is-a关系
核心思想:public继承必须严格遵循"is-a"关系(里氏替换原则),即派生类对象在任何场景下都必须是基类对象的逻辑子集。违反这一原则将导致设计错误和运行时异常。
⚠️ 1. is-a关系的内涵与要求
基本原则:
- 派生类对象可被当作基类对象使用
- 基类的所有行为都适用于派生类
- 派生类可扩展功能,但不能削弱基类契约
数学表达:
∀x (x∈Derived → x∈Base) // 派生类是基类的子集
代码验证:
void process(const Base& b); // 接受基类的函数
Derived d;
process(d); // 必须能正确工作(里氏替换)
违反示例:
class Bird {
public:
virtual void fly(); // 鸟会飞
};
class Penguin : public Bird { // 企鹅是鸟,但不会飞!
public:
void fly() override {
throw CantFlyException(); // 违反is-a!
}
};
Penguin p;
processBird(p); // 可能抛出异常,破坏基类契约
🚨 2. 常见设计陷阱与解决方案
陷阱1:非普适行为继承
// 错误设计
class Rectangle {
public:
virtual void setHeight(int h); // 可独立设置宽高
};
class Square : public Rectangle {
public:
void setHeight(int h) override {
width_ = h; // 同时修改宽度
height_ = h;
}
// 违反矩形行为契约!
};
void stretch(Rectangle& r) {
r.setHeight(r.getHeight() + 10); // 对正方形会意外修改宽度
}
解决方案:
// 正确设计:不继承
class Square {
public:
void setSide(int s) { ... }
};
// 关系建模
class Shape { /* 公共接口 */ };
class Rectangle : public Shape { ... };
class Square : public Shape { ... };
陷阱2:接口污染
class Airport { ... };
// 错误设计
class Aircraft {
public:
virtual void takeoff(Airport& dest) = 0;
};
class HeliPad; // 直升机专用
class Helicopter : public Aircraft {
public:
void takeoff(HeliPad& pad); // 参数类型不兼容!
};
解决方案:
// 正确设计:分离接口
class Aircraft {
public:
virtual void takeoff() = 0;
};
class CommercialAircraft : public Aircraft {
public:
void setDestination(Airport& ap);
void takeoff() override;
};
class Helicopter : public Aircraft {
public:
void setLaunchPad(HeliPad& pad);
void takeoff() override;
};
⚖️ 3. 最佳实践指南
场景 | 推荐方案 | 原因 |
---|---|---|
严格is-a关系 | ✅ 使用public继承 | 天然建模分类关系 |
共享接口但行为不同 | 🔶 接口继承+实现覆盖 | 保持多态行为一致 |
“has-a"或"is-implemented-in-terms-of” | ⛔ 避免继承,用组合 | 防止接口污染 |
运行时类型依赖 | ⚠️ 避免dynamic_cast | 通常是设计缺陷的信号 |
接口扩展 | ✅ 非虚拟接口模式(NVI) | 保持核心策略稳定 |
现代C++增强:
// 明确禁止覆盖(C++11)
class Base {
public:
virtual void stableAPI() final; // 禁止派生类修改
};
// 显式重写语法(C++11)
class Derived : public Base {
public:
void stableAPI() override; // 错误!final函数不能override
};
// 契约编程(C++20)
class Shape {
public:
virtual void draw()
[[expects: isValid()]] // 前置条件
[[ensures: isDrawn()]] // 后置条件
= 0;
};
💡 关键设计原则
里氏替换测试
- 派生类对象必须能替代基类对象
- 所有基类操作在派生类中保持语义一致
- 派生类不强化前置条件/不弱化后置条件
契约继承优先
- 继承行为契约而非具体实现
- 使用纯虚函数定义严格接口
- 模板方法模式控制流程
组合优于继承
// "has-a"关系使用组合 class Car { private: Engine engine_; // Car has-a Engine }; // "is-implemented-in-terms-of"使用组合 class Set { private: std::list<int> impl_; // 用list实现set public: void insert(int v) { if(!contains(v)) impl_.push_back(v); } };
类型特征检查
// C++17编译时检查 template<typename T> void process(T obj) { static_assert(std::is_base_of_v<Base, T>, "T must inherit from Base"); // ... }
危险模式重现:
class Database { public: virtual void open() = 0; }; class MySQL : public Database { public: void open() override; // 需要连接参数? }; void runReport(Database& db) { db.open(); // 对MySQL可能缺少必要参数 }
安全重构方案:
class Database { public: void open(const ConnectionParams& params) { // 非虚接口 validate(params); doOpen(params); // 虚函数 } private: virtual void doOpen(const ConnectionParams&) = 0; }; class MySQL : public Database { private: void doOpen(const ConnectionParams& params) override { // 使用参数建立连接 } };
多态安全场景:
class Animal { public: virtual void move() = 0; }; class Bird : public Animal { public: void move() override { fly(); } virtual void fly() { /* 飞行实现 */ } }; class Ostrich : public Bird { // 鸵鸟是鸟但不会飞 public: void fly() override { throw CannotFlyError(); // 设计错误! } }; // 正确设计:分离会飞行为 class FlyingAnimal : public Animal { public: void move() override { fly(); } virtual void fly() = 0; }; class Bird : public Animal { /* 不强制飞行 */ }; class Eagle : public Bird, public FlyingAnimal { ... }; class Ostrich : public Bird { /* 实现行走 */ };