Effective C++ 条款34:区分接口继承和实现继承

Effective C++ 条款34:区分接口继承和实现继承

public 继承看似简单,实则包含两个可分离的部分:接口继承与实现继承。
理解它们的区别,是设计出优雅继承体系的关键一步。


一、问题的提出:继承的两种含义

在 C++ 中,当我们写下这样的代码时,到底在继承什么?

class Base {
public:
    virtual void func1() = 0;        // pure virtual
    virtual void func2();             // impure virtual
    void func3();                     // non-virtual
};

class Derived : public Base {
    // Derived 从 Base 继承了什么?
};

表面上看,Derived 继承了 Base 的三个函数。但深入思考会发现:

  • func1 没有实现,派生类必须自己实现
  • func2 有默认实现,派生类可以覆盖也可以不覆盖
  • func3 有固定实现,派生类不应该覆盖

这三种函数代表了三种截然不同的继承语义。混淆它们,是继承设计中常见的错误。


二、三种成员函数的继承语义

2.1 Pure Virtual 函数:只继承接口

class Shape {
public:
    virtual ~Shape() = default;
    
    // Pure virtual 函数:只指定接口,不指定实现
    virtual void draw() const = 0;
    virtual double area() const = 0;
    virtual double perimeter() const = 0;
};

class Circle : public Shape {
public:
    Circle(double radius) : radius_(radius) {}
    
    // 必须实现所有 pure virtual 函数
    void draw() const override {
        std::cout << "绘制圆形,半径=" << radius_ << "\n";
    }
    
    double area() const override {
        return 3.14159 * radius_ * radius_;
    }
    
    double perimeter() const override {
        return 2 * 3.14159 * radius_;
    }

private:
    double radius_;
};

class Rectangle : public Shape {
public:
    Rectangle(double w, double h) : width_(w), height_(h) {}
    
    void draw() const override {
        std::cout << "绘制矩形," << width_ << "x" << height_ << "\n";
    }
    
    double area() const override {
        return width_ * height_;
    }
    
    double perimeter() const override {
        return 2 * (width_ + height_);
    }

private:
    double width_, height_;
};

Pure Virtual 函数的设计意图:

特性说明
接口契约定义派生类必须支持的接口
强制实现派生类必须提供自己的实现,否则无法实例化
抽象基类包含 pure virtual 的类是抽象类,不能创建对象
多态基础为运行时多态提供统一的调用接口

令人意外的事实: C++ 允许为 pure virtual 函数提供定义!

class Shape {
public:
    // 声明为 pure virtual,但仍可提供默认实现
    virtual void draw() const = 0;
    virtual double area() const = 0;
};

// 为 pure virtual 函数提供定义
void Shape::draw() const {
    std::cout << "默认绘制实现\n";
}

double Shape::area() const {
    return 0.0;  // 默认面积
}

class Triangle : public Shape {
public:
    // 可以选择调用基类的默认实现
    void draw() const override {
        Shape::draw();  // 调用 pure virtual 的默认实现
        std::cout << "三角形自定义绘制\n";
    }
    
    double area() const override {
        // 必须提供自己的计算
        return base_ * height_ / 2;
    }

private:
    double base_, height_;
};

这种"有定义的 pure virtual 函数"的用途:为派生类提供一个可选的默认实现,但强制派生类显式决定是否使用它。

2.2 Impure Virtual 函数:继承接口和默认实现

class Animal {
public:
    virtual ~Animal() = default;
    
    // Impure virtual:继承接口 + 默认实现
    virtual void makeSound() const {
        std::cout << "某种动物叫声\n";  // 默认实现
    }
    
    virtual void move() const {
        std::cout << "动物在移动\n";     // 默认实现
    }
};

class Dog : public Animal {
public:
    // 覆盖默认实现,提供特定行为
    void makeSound() const override {
        std::cout << "汪汪!\n";
    }
    // move() 继承默认实现,不需要覆盖
};

class Cat : public Animal {
public:
    void makeSound() const override {
        std::cout << "喵喵~\n";
    }
};

class Fish : public Animal {
public:
    void makeSound() const override {
        std::cout << "...(鱼不会叫)\n";
    }
    
    void move() const override {
        std::cout << "鱼在游泳\n";
    }
};

Impure Virtual 函数的设计意图:

特性说明
接口 + 默认实现派生类继承函数的签名和默认行为
可选覆盖派生类可以选择使用默认实现或提供自己的实现
代码复用为大多数派生类提供通用实现,减少重复代码

2.3 Non-Virtual 函数:继承接口和强制实现

class Base {
public:
    // Non-virtual 函数:接口 + 强制实现
    void algorithm() {
        // 算法的框架,不允许派生类改变
        step1();
        step2();
        step3();
    }
    
    virtual ~Base() = default;

protected:
    // 这些步骤派生类可以定制
    virtual void step1() = 0;
    virtual void step2() = 0;
    virtual void step3() = 0;
};

// 更典型的例子
class Clock {
public:
    // 获取当前时间:所有时钟的统一接口,不允许覆盖
    Time getCurrentTime() const {
        return readHardwareClock();
    }
    
    // 格式化显示:统一的显示方式
    std::string formatTime(const Time& t) const {
        return t.toString("YYYY-MM-DD HH:MM:SS");
    }

private:
    virtual Time readHardwareClock() const = 0;  // 硬件相关,由派生类实现
};

class SystemClock : public Clock {
private:
    Time readHardwareClock() const override {
        // 读取系统时钟
        return Time::now();
    }
};

class AtomicClock : public Clock {
private:
    Time readHardwareClock() const override {
        // 读取原子钟
        return fetchAtomicTime();
    }
};

Non-Virtual 函数的设计意图:

特性说明
不变性表示派生类不应该改变的行为
一致性确保所有派生类的某个行为完全一致
静态绑定调用在编译期确定,性能更好

三、三种函数的对比总结

class Base {
public:
    // 1. Pure Virtual:只继承接口
    virtual void interfaceOnly() = 0;
    
    // 2. Impure Virtual:继承接口 + 默认实现
    virtual void interfaceWithDefault() {
        std::cout << "默认实现\n";
    }
    
    // 3. Non-Virtual:继承接口 + 强制实现
    void interfaceWithMandatory() {
        std::cout << "这是唯一实现,不允许覆盖\n";
    }
};
函数类型继承接口?继承实现?派生类能否覆盖?设计意图
Pure Virtual否(可选项)必须覆盖“你必须实现这个功能”
Impure Virtual是(默认)可选覆盖“你可以使用默认实现,也可以自定义”
Non-Virtual是(强制)不应该覆盖“所有派生类的这个行为必须一致”

四、一个完整的实际案例:游戏 AI 系统

让我们用一个游戏 AI 系统来展示三种函数的正确使用:

#include <iostream>
#include <string>
#include <vector>
#include <memory>

// AI 行为基类:定义所有 AI 的通用接口
class AIBehavior {
public:
    virtual ~AIBehavior() = default;
    
    // ========== Pure Virtual:每个 AI 必须有自己的实现 ==========
    
    // 评估当前状态并决定下一步行动
    virtual void evaluate() = 0;
    
    // 执行选定的行动
    virtual void execute() = 0;
    
    // 获取 AI 的名称
    virtual std::string getName() const = 0;
    
    // ========== Impure Virtual:有默认实现,但可覆盖 ==========
    
    // 更新 AI 状态(每帧调用)
    // 默认实现:先评估,再执行
    virtual void update(float deltaTime) {
        evaluate();
        if (hasActionSelected()) {
            execute();
        }
    }
    
    // 受到伤害时的反应
    // 默认实现:简单的退缩
    virtual void onDamageTaken(int damage) {
        std::cout << getName() << " 受到 " << damage << " 点伤害!\n";
        health_ -= damage;
        if (health_ <= 0) {
            onDeath();
        }
    }
    
    // ========== Non-Virtual:所有 AI 的共同行为,不允许改变 ==========
    
    // 获取当前生命值(所有 AI 的生命值计算方式相同)
    int getHealth() const {
        return health_;
    }
    
    // 检查 AI 是否存活(统一的判断逻辑)
    bool isAlive() const {
        return health_ > 0;
    }
    
    // 注册到 AI 管理器(统一的注册流程)
    void registerToManager(AIManager& manager) {
        manager.registerAI(this);
        onRegistered();
    }

protected:
    int health_ = 100;
    bool hasAction_ = false;
    
    bool hasActionSelected() const { return hasAction_; }
    
    // 派生类可以覆盖的钩子
    virtual void onDeath() {
        std::cout << getName() << " 死亡。\n";
    }
    
    virtual void onRegistered() {}
};

// 具体的 AI 实现:巡逻的守卫
class PatrolGuardAI : public AIBehavior {
public:
    std::string getName() const override {
        return "巡逻守卫";
    }
    
    void evaluate() override {
        // 检查视野内是否有敌人
        if (detectEnemy()) {
            selectedAction_ = Action::Attack;
            hasAction_ = true;
        } else if (shouldPatrol()) {
            selectedAction_ = Action::Patrol;
            hasAction_ = true;
        } else {
            hasAction_ = false;
        }
    }
    
    void execute() override {
        switch (selectedAction_) {
            case Action::Attack:
                std::cout << "守卫发现敌人,发起攻击!\n";
                break;
            case Action::Patrol:
                std::cout << "守卫继续巡逻...\n";
                break;
        }
    }
    
    // 覆盖默认的伤害反应:守卫会呼叫支援
    void onDamageTaken(int damage) override {
        AIBehavior::onDamageTaken(damage);  // 先调用默认处理
        if (isAlive()) {
            std::cout << "守卫呼叫支援!\n";
            callForBackup();
        }
    }

private:
    enum class Action { Attack, Patrol };
    Action selectedAction_;
    
    bool detectEnemy() { /* ... */ return false; }
    bool shouldPatrol() { /* ... */ return true; }
    void callForBackup() { /* ... */ }
};

// 具体的 AI 实现:Boss 怪物
class BossAI : public AIBehavior {
public:
    std::string getName() const override {
        return "Boss";
    }
    
    void evaluate() override {
        // Boss 有更复杂的决策逻辑
        if (health_ < 30) {
            selectedAction_ = Action::Enrage;
            hasAction_ = true;
        } else if (canUseSpecialAttack()) {
            selectedAction_ = Action::SpecialAttack;
            hasAction_ = true;
        } else {
            selectedAction_ = Action::NormalAttack;
            hasAction_ = true;
        }
    }
    
    void execute() override {
        switch (selectedAction_) {
            case Action::Enrage:
                std::cout << "Boss 进入狂暴状态!\n";
                attackPower_ *= 2;
                break;
            case Action::SpecialAttack:
                std::cout << "Boss 释放必杀技!\n";
                break;
            case Action::NormalAttack:
                std::cout << "Boss 普通攻击。\n";
                break;
        }
    }
    
    // Boss 覆盖默认更新:狂暴时更新频率更高
    void update(float deltaTime) override {
        if (isEnraged_) {
            // 狂暴时更新两次
            AIBehavior::update(deltaTime);
            AIBehavior::update(deltaTime);
        } else {
            AIBehavior::update(deltaTime);
        }
    }

private:
    enum class Action { Enrage, SpecialAttack, NormalAttack };
    Action selectedAction_;
    int attackPower_ = 50;
    bool isEnraged_ = false;
    
    bool canUseSpecialAttack() { /* ... */ return false; }
};

// 使用示例
class AIManager {
public:
    void registerAI(AIBehavior* ai) {
        aiList_.push_back(ai);
    }
    
    void updateAll(float deltaTime) {
        for (auto* ai : aiList_) {
            if (ai->isAlive()) {  // Non-virtual,统一的存活检查
                ai->update(deltaTime);  // Virtual,调用各自的更新逻辑
            }
        }
    }

private:
    std::vector<AIBehavior*> aiList_;
};

设计分析:

函数类型设计理由
evaluate() / execute() / getName()Pure Virtual每种 AI 的行为都不同,必须各自实现
update() / onDamageTaken()Impure Virtual大多数 AI 使用默认逻辑,但特殊 AI(如 Boss)可以覆盖
getHealth() / isAlive() / registerToManager()Non-Virtual所有 AI 的这些行为应该一致,不允许改变

五、常见陷阱:默认实现的危险

Impure virtual 函数虽然方便,但也存在隐患:

class Airplane {
public:
    virtual void fly() {
        // 默认实现:普通飞机的飞行方式
        std::cout << "使用默认飞行算法\n";
    }
};

class Boeing747 : public Airplane {
    // 使用默认的 fly() 实现——合理
};

class ModelC172 : public Airplane {
    // 使用默认的 fly() 实现——合理
};

// 危险:新增一种飞机,忘记覆盖 fly()
class SpaceShuttle : public Airplane {
    // 糟糕!航天飞机不应该使用普通飞机的飞行算法!
    // 但编译器不会报错,因为 fly() 有默认实现
};

解决方案: 将接口和默认实现分离

class Airplane {
public:
    // 纯虚函数:只声明接口
    virtual void fly() = 0;
    
protected:
    // 默认实现单独提供,派生类必须显式选择是否使用
    void defaultFly() {
        std::cout << "使用默认飞行算法\n";
    }
};

class Boeing747 : public Airplane {
public:
    void fly() override {
        defaultFly();  // 显式选择使用默认实现
    }
};

class SpaceShuttle : public Airplane {
public:
    void fly() override {
        // 必须自己实现,无法意外使用默认版本
        std::cout << "使用航天飞机专用飞行算法\n";
    }
};

六、设计决策流程图

当你在设计基类时,可以用以下流程决定函数的类型:

设计一个成员函数:
│
├─ 派生类必须提供自己的实现?
│  ├─ 是 → 使用 Pure Virtual (= 0)
│  └─ 否 → 继续问:
│
├─ 派生类可能需要不同的实现?
│  ├─ 是 → 使用 Impure Virtual (提供默认实现)
│  └─ 否 → 使用 Non-Virtual
│
└─ 是否担心派生类忘记覆盖?
   ├─ 是 → 使用 Pure Virtual + protected 默认实现
   └─ 否 → 使用 Impure Virtual

七、总结

函数类型继承内容使用场景
Pure Virtual仅接口定义派生类必须实现的契约
Impure Virtual接口 + 默认实现提供通用行为,允许特殊情况覆盖
Non-Virtual接口 + 强制实现确保所有派生类行为一致

请记住:

  • 接口继承和实现继承不同。在 public 继承之下,derived classes 总是继承 base class 的接口。
  • pure virtual 函数只具体指定接口继承。
  • 简朴的(非纯)impure virtual 函数具体指定接口继承及缺省实现继承。
  • non-virtual 函数具体指定接口继承以及强制性实现继承。

正确区分这三种函数类型,并理解它们背后的设计意图,是创建清晰、健壮、可维护的继承体系的基础。每一个 virtual/non-virtual 的选择,都应该是有意识的设计决策,而不是随意的编码习惯。


参考:《Effective C++》第三版,Scott Meyers 著

相关条款:条款32(确定 public 继承塑模出 is-a 关系)、条款33(避免遮掩继承而来的名字)、条款35(考虑 virtual 函数以外的其他选择)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

凡人叶枫

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值