6Effective C++(第3版)

继承与面向对象设计

继承设计

public继承非虚/虚/纯虚函数NVI惯用法菱形继承

🎯 AI 时代的学习建议:不要只读理论。理解这一章的设计动机(为什么需要这个概念?)和边界条件(什么情况下会失效?)。当 AI 生成的代码涉及这些概念时,你能判断它是否用对了场景。AI 能帮你总结内容,但无法替代你亲手阅读、思考、建立直觉的过程。

第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 惯用法设计一个数据库连接类,包含连接前验证和断开后清理。

设计一个图形类层次,正确使用虚函数和纯虚函数。


6/9继承与面
0%