Item34--区分接口继承和实现继承

这个 Item 讨论的是纯虚函数非纯虚函数(普通虚函数)和非虚函数在设计意图上的巨大差异。


1. 核心概念:三种函数的不同语义

public 继承体系下,基类的成员函数代表了三种不同的继承契约:

A. 纯虚函数 (Pure Virtual Functions)

  • 语法: virtual void draw() const = 0;
  • 含义: 只继承接口
  • 设计意图: “派生类必须提供这个功能,而且必须自己去实现它。”
  • 适用场景: 抽象的概念(如 Shape::draw()),基类无法提供合理的默认实现。

B. 非纯虚函数 (Impure / Simple Virtual Functions)

  • 语法: virtual void error(const std::string& msg);
  • 含义: 继承接口 + 继承一份默认实现
  • 设计意图: “派生类必须提供这个功能,如果你不想自己写,可以用我提供的默认版本。”
  • 适用场景: 大多数派生类的行为一致,但允许特例存在。

C. 非虚函数 (Non-Virtual Functions)

  • 语法: int objectID() const;
  • 含义: 继承接口 + 继承强制实现
  • 设计意图: “派生类必须继承这个功能,而且绝对不能修改它的行为(不变性)。”
  • 注意: 如果你在派生类中重新定义了非虚函数,就违反了 Item 36(绝不重新定义继承而来的 non-virtual 函数)。

2. “默认实现”的陷阱:飞机案例

Item 34 最精彩的部分在于指出了非纯虚函数(B类)带来的潜在风险。

场景:

假设你在设计一个航空公司系统。只有 A 型和 B 型飞机,它们的飞行方式完全一样。

class Airplane {
public:
    virtual void fly(const Airport& destination) {
        // 缺省行为:像普通客机一样飞行
        // ...飞往目的地的代码...
    }
};

class ModelA : public Airplane { /* ... */ };
class ModelB : public Airplane { /* ... */ };

此时,ModelAModelB 都不需要写 fly 函数,直接继承基类的默认实现。这很方便,代码复用率高。

问题:

现在,公司引入了 C 型飞机(ModelC),但这并不是普通客机,而是一种滑翔机(它的飞行逻辑完全不同)。

如果你在写 ModelC 时,忘记了重写 fly 函数:

class ModelC : public Airplane {
    // 糟糕!程序员忘记声明 fly() 了
};

后果: 代码编译完全通过。但是当 ModelC 起飞时,它会试图调用 Airplane::fly 的默认逻辑(像喷气式客机一样飞),这可能会导致严重的逻辑错误(比如滑翔机没有引擎,却执行了引擎点火逻辑)。

根本原因: 普通虚函数提供了“接口”和“默认实现”,这不仅赋予了派生类使用默认实现的权利,也隐含了直接继承默认实现的风险


3. 安全的解决方案:分离接口与默认实现

为了避免上述悲剧,我们需要一种机制:提供默认实现,但迫使派生类必须显式地请求使用它,而不是如果不小心就自动继承了它。

Scott Meyers 提供了两种优雅的方案:

方案一:纯虚函数 + protected 辅助函数

fly 变成纯虚函数(强制派生类必须实现),然后将原本的默认逻辑放入一个独立的 protected 函数中。

class Airplane {
public:
    // 1. 纯虚函数:强制派生类必须自己声明 fly
    virtual void fly(const Airport& destination) = 0; 

protected:
    // 2. 将默认逻辑剥离出来
    void defaultFly(const Airport& destination) {
        // ... 普通飞机的飞行代码 ...
    }
};

class ModelA : public Airplane {
public:
    virtual void fly(const Airport& destination) override {
        defaultFly(destination); // 显式请求默认行为
    }
};

class ModelC : public Airplane {
public:
    virtual void fly(const Airport& destination) override {
        // ... 实现滑翔机的独特飞行逻辑 ...
    }
};
  • 优点: 如果 ModelC 忘记写 fly,编译器会报错(因为基类 fly 是纯虚的)。
  • 缺点: 污染了命名空间(多了一个 defaultFly 函数)。

方案二:为纯虚函数提供定义(推荐)

这是一个很酷但鲜为人知的 C++ 特性:纯虚函数也可以有函数体

class Airplane {
public:
    virtual void fly(const Airport& destination) = 0;
};

// 是的,纯虚函数可以有实现!
void Airplane::fly(const Airport& destination) {
    // ... 普通飞机的默认飞行代码 ...
}

class ModelA : public Airplane {
public:
    virtual void fly(const Airport& destination) override {
        Airplane::fly(destination); // 显式调用基类的默认实现
    }
};

class ModelC : public Airplane {
public:
    virtual void fly(const Airport& destination) override {
        // ... 实现滑翔机逻辑,不调用 Airplane::fly ...
    }
};
  • 机制: virtual ... = 0 迫使 ModelAModelC 必须重写 fly。但如果 ModelA 想要默认行为,它可以调用 Airplane::fly
  • 优点: 既强制了接口继承(必须重写),又提供了实现继承(可以复用),而且没有引入新的函数名。

4. 总结

在设计基类成员函数时,请根据你的意图选择正确的声明方式:

声明方式 继承了什么? 设计意图(潜台词)
纯虚函数 virtual void f() = 0; 仅接口 “你必须实现这个函数。我不提供默认版本(或者虽然提供了,但你必须显式调用)。”
非纯虚函数 virtual void f(); 接口 + 默认实现 “你应该实现这个函数。但如果你不写,可以用我提供的通用版本。”(小心:可能导致意外继承不该有的行为)
非虚函数 void f(); 接口 + 强制实现 “你必须继承这个函数,且不得修改它的行为。”
posted @ 2025-12-20 21:56  belief73  阅读(2)  评论(0)    收藏  举报