第6章:继承与面向对象设计
一、导读
1.1 本章学习目标
第6章讲解 C++ 继承和面向对象设计的最佳实践。Meyers 在此深入分析了 public 继承、虚函数、NVI 惯用法等核心概念。
本章核心学习目标:
- 理解 public 继承的含义:is-a 关系
- 掌握虚函数的使用:非虚/虚/纯虚函数的选择
- 理解 NVI 惯用法:Non-Virtual Interface
- 掌握菱形继承和虚继承
1.2 为什么本章如此重要
继承是 C++ 中最容易被误用的特性之一。错误的继承设计会导致脆弱的代码和难以维护的类层次。
1.3 核心问题引导
public 继承意味着什么?什么时候不应该使用 public 继承?
为什么不应该重新定义继承而来的非虚函数?
NVI 惯用法解决了什么问题?
二、核心概念详解
2.1 public 继承意味着 is-a
cpp
// 正确: public 继承表示 is-a
class Bird {
public:
virtual void fly() { /* 飞 */ }
};
class Sparrow : public Bird { }; // 麻雀是一种鸟 ✓
// 错误: 违反 is-a
class Penguin : public Bird { }; // 企鹅是一种鸟?但企鹅不会飞!
// penguin.fly() 编译通过但运行时报错 → 设计错误
// 正确做法:
class Bird { }; // 不声明 fly
class FlyingBird : public Bird {
public:
virtual void fly() { /* 飞 */ }
};
class Penguin : public Bird { }; // 企鹅是鸟,但不会飞 ✓
class Sparrow : public FlyingBird { }; // 麻雀是会飞的鸟 ✓2.2 虚函数的选择
cpp
class Shape {
public:
// 非虚函数: 不希望子类重写
void draw() const {
// 通用绘制逻辑
setupCanvas();
doDraw(); // 调用子类的实现
cleanupCanvas();
}
// 纯虚函数: 强制子类实现
virtual void doDraw() const = 0;
// 虚函数: 提供默认实现,子类可重写
virtual void fill() const { /* 默认填充 */ }
virtual ~Shape() = default;
private:
void setupCanvas() { /* ... */ }
void cleanupCanvas() { /* ... */ }
};
// NVI 惯用法: 非虚接口 + 虚实现
// draw 是非虚的(模板方法),doDraw 是虚的(子类定制)2.3 不要重新定义继承而来的非虚函数
cpp
class Base {
public:
void mf() { /* Base 实现 */ }
};
class Derived : public Base {
public:
void mf() { /* Derived 实现 */ } // 隐藏了 Base::mf
};
Base* ptr = new Derived;
ptr->mf(); // 调用 Base::mf!因为 mf 不是虚函数
// 原因: 非虚函数是静态绑定的(编译时确定)
// 虚函数是动态绑定的(运行时确定)三、实战案例
3.1 案例一:NVI 惯用法实践
cpp
class GameCharacter {
public:
int healthValue() const { // 非虚接口
// 前置处理
logAccess("healthValue");
// 调用虚函数(子类可定制)
int result = doHealthValue();
// 后置处理
if (result < 0) result = 0;
return result;
}
virtual ~GameCharacter() = default;
private:
virtual int doHealthValue() const { // 虚实现
return baseHealth + armorBonus;
}
void logAccess(const char* method) const { /* ... */ }
int baseHealth = 100;
int armorBonus = 10;
};
class Elf : public GameCharacter {
private:
int doHealthValue() const override {
return GameCharacter::doHealthValue() + magicBonus;
}
int magicBonus = 20;
};
class Troll : public GameCharacter {
private:
int doHealthValue() const override {
return GameCharacter::doHealthValue() * 2; // 双倍血量
}
};四、本章小结
public 继承意味着 is-a,子类必须能替代基类使用
虚函数提供动态绑定,纯虚函数强制子类实现
NVI 惯用法通过非虚接口+虚实现,在框架中插入前置/后置处理
五、思考与练习
思考题
为什么析构函数应该是虚的?
NVI 和 Template Method 模式有什么关系?
编程练习
用 NVI 惯用法设计一个数据库连接类,包含连接前验证和断开后清理。
设计一个图形类层次,正确使用虚函数和纯虚函数。