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 函数以外的其他选择)
879

被折叠的 条评论
为什么被折叠?



