Effective C++ 条款34:区分接口继承和实现继承
Effective C++ 条款34:区分接口继承和实现继承
核心思想:public继承中,成员函数的不同声明方式对应着不同的继承语义:纯虚函数只继承接口,虚函数继承接口和默认实现,非虚函数继承接口和强制实现。错误混用会导致设计脆弱性。
⚠️ 1. 三种继承方式的本质区别
继承类型对照表:
函数类型 | 接口继承 | 实现继承 | 设计意图 | 风险 |
---|---|---|---|---|
纯虚函数 | ✅ | ❌ | 强制派生类提供实现 | 无默认实现可能代码重复 |
虚函数 | ✅ | ✅(可选) | 提供默认实现,允许派生类覆盖 | 默认实现可能不适用所有派生 |
非虚函数 | ✅ | ✅(强制) | 定义不可更改的通用行为 | 派生类无法定制行为 |
代码验证:
class Shape {
public:// 纯虚函数:只继承接口virtual void draw() const = 0;// 虚函数:继承接口+默认实现virtual void rotate(int degrees) { // 默认旋转逻辑}// 非虚函数:继承接口+强制实现int id() const { return id_; }
};class Circle : public Shape {
public:void draw() const override; // 必须实现// rotate()可选:使用默认或覆盖// id()不可覆盖,强制使用基类实现
};
🚨 2. 关键陷阱与解决方案
陷阱1:虚函数的不安全默认实现
class Aircraft {
public:virtual void fly(const Airport& dest) {defaultFly(dest); // 默认飞行逻辑}
};class ModelA : public Aircraft {}; // 使用默认flyclass ModelB : public Aircraft {
public:void fly(const Airport& dest) override {customFly(dest); // 自定义飞行}
};// 危险:新增机型忘记覆盖fly
class ModelX : public Aircraft {}; // 意外使用默认飞行逻辑!
解决方案1:分离接口与默认实现
class Aircraft {
public:void fly(const Airport& dest) { // 非虚接口(NVI)doFly(dest); }
private:virtual void doFly(const Airport& dest) = 0; // 强制派生类实现
protected:void defaultFly(const Airport& dest); // 提供默认实现(可选)
};class ModelX : public Aircraft {
private:void doFly(const Airport& dest) override {defaultFly(dest); // 显式选择使用默认}
};
解决方案2:安全提供默认实现
class Aircraft {
public:virtual void fly(const Airport& dest) = 0;
protected:void defaultFly(const Airport& dest); // 保护方法
};// 派生类明确选择
class ModelX : public Aircraft {
public:void fly(const Airport& dest) override {defaultFly(dest); // 安全使用默认}
};
⚖️ 3. 最佳实践指南
设计目标 | 推荐方案 | 示例 |
---|---|---|
强制接口规范 | ✅ 纯虚函数 | virtual void render() = 0; |
安全默认实现 | 🔶 纯虚函数+保护默认方法 | protected: void defaultRender(); |
允许覆盖的通用行为 | ⚠️ 虚函数(谨慎使用) | virtual void save(); |
不可更改的通用行为 | ✅ 非虚函数 | int serialNumber() const; |
流程控制固定,步骤可定制 | ✅ 非虚接口模式(NVI) | 见下文完整示例 |
现代C++增强:
// C++11 override/final
class Widget {
public:virtual void process() final; // 禁止进一步覆盖
};class AdvancedWidget : public Widget {
public:void process() override; // 错误!final函数不可覆盖
};// C++20 contract + virtual
class Database {
public:virtual void commit() [[expects: isConnected()]] [[ensures: dataPersisted()]] = 0;
};
💡 关键设计原则
-
非虚接口(NVI)模式
class GameCharacter { public:// 非虚公开接口(控制流程框架)int healthValue() const {// 前置处理(如日志、锁)int val = doHealthValue();// 后置处理(如验证、通知)return postProcess(val);} private:// 实现细节委托给派生类virtual int doHealthValue() const {// 默认实现(可选)return calculateBaseHealth();}// 可添加更多扩展点... };
-
纯虚函数的安全默认
class Printer { public:virtual void print(Document& doc) = 0; protected:// 安全提供默认实现void defaultPrint(Document& doc) {// 通用打印逻辑} };class InkjetPrinter : public Printer { public:void print(Document& doc) override {preHeat();defaultPrint(doc); // 显式调用postCool();} };
-
非虚函数的谨慎使用
class Account { public:// 非虚函数:通用且不可更改的行为double balance() const {std::lock_guard<std::mutex> lock(mutex_);return balance_;} private:mutable std::mutex mutex_;double balance_; };// 所有账户类型使用相同线程安全实现 class SavingsAccount : public Account {}; class CheckingAccount : public Account {};
危险模式重现:
class DataSource { public:virtual void load() { // 虚函数提供默认实现// 从文件加载的默认逻辑} };class DatabaseSource : public DataSource {// 忘记覆盖load,错误使用文件加载! };class NetworkSource : public DataSource { public:void load() override {// 正确网络加载实现} };
安全重构方案:
class DataSource { public:void load() { // 非虚接口doLoad();validateData();} private:virtual void doLoad() = 0; // 强制派生实现 };class DatabaseSource : public DataSource { private:void doLoad() override {// 必须实现数据库加载} };
模板方法模式应用:
class ReportGenerator { public:// 模板方法(非虚)void generateReport() {openDataSource();extractData(); // 虚函数钩子convertFormat(); // 虚函数钩子closeDataSource();} protected:virtual void extractData() = 0;virtual void convertFormat() {// 默认格式转换} };class PDFReport : public ReportGenerator { protected:void extractData() override { /* PDF提取 */ }// 使用默认convertFormat };