🔷 第十一章 继承与派生
📄 一句话概括

纯虚函数写法特殊:virtual 返回类型 函数名(参数) = 0;——末尾那个 = 0 表示"我这里不给实现,交给子类"。含有纯虚函数的类就叫抽象类,它有两条硬规则:① 不能创建对象(Shape s; 编译错);② 子类必须把全部纯虚函数都实现出来,才能创建对象。它的价值在于描述"必须会做什么",而不规定"怎么做"——这就是接口。

📝 本节和前面几节的关系

本节是 11.4.2 的自然升级:11.4.2 里 Shape::area() 给了一个"保底实现(返回 0)"——但 Shape 的面积"返回 0"这件事本身就没有意义。纯虚函数就是把这种"没意义的保底实现"删掉、改成 = 0,同时顺手禁止别人创建 Shape 对象。它也是整章知识的汇合点:继承(11.1)+ 虚函数(11.4.1)+ 动态绑定(11.4.2)三者最终服务于"用抽象类定义接口"。

📝 本节的教材说明

教材在 11.5 之下还有一个未编号的主题 "创建纯虚函数",以及实例08。按本课程"逐小节一篇"的粒度,它们并入本篇,不单独成篇。

一、纯虚函数:= 0

class Shape {
public:
    virtual double area() const = 0;     // ★ 纯虚函数:注意末尾 = 0
};

对比普通虚函数:

virtual double area() const { return 0; }     // 普通虚函数:有实现(哪怕是"保底")
virtual double area() const = 0;              // 纯虚函数:没实现,末尾 = 0

= 0 是什么:它不是"赋值为 0",而是一个标记语——"这个函数是'纯'的:我在父类里不提供实现,子类必须自己给。"

三条语法要点:

要点 说明
只能是虚函数才能"纯" 必须写 virtual(virtual ... = 0),不写 virtual 直接 = 0 是错的
不能有函数体 virtual void f() = 0 { } ❌(要写体就不叫纯虚了)
= 0 写在声明末尾,分号之前 virtual void f() = 0;

冷知识(知道就好):纯虚函数也可以有实现——只是必须写在类外,而且只能通过 父类名::函数名() 显式调用(子类不会自动用它)。实践里几乎不用,看到不用惊讶即可。

二、抽象类与它的两条硬规则

"含至少一个纯虚函数的类"=抽象类(abstract class)。

规则 现场
① 不能创建对象 Shape s; → 错:cannot declare variable 's' to be of abstract type 'Shape'
② 子类必须实现全部纯虚函数才能创建对象 少实现一个 → 这个子类也是抽象类,照样不能创建对象
③ 但可以定义指针和引用 Shape *p; ✅、Shape &r = circle; ✅——这正是多态要用的形态(11.4.2)

为什么"不能创建对象":因为抽象类"不完整"——它规定了"必须有 area()",但没告诉你怎么算。造一个"不知道面积怎么算的形状",本身就没有意义。编译器直接把它禁掉,比让你在运行时得到乱七八糟的结果要好。

第②条的具体样子:

class Shape {
public:
    virtual double area() const = 0;
    virtual const char *name() const = 0;
};

class Circle : public Shape {
private:
    double r;
public:
    Circle(double radius) : r(radius) {}
    double area() const { return 3.14159 * r * r; }
    // 忘了实现 name()!
};

Circle c(1);        // ❌ 错:abstract type —— 因为 name() 还是纯虚的

报错会说 because the following virtual functions are pure within 'Circle'——编译器会把"你还没实现的那些纯虚函数"列出来,非常贴心。

三、抽象类的用途:定义"接口"

抽象类表达的是"契约":

"凡是自称是我的子类,就必须会做这些事。"

它的三个典型用途:

用途 例子
统一一批"同类但实现不同"的东西 Shape → Circle / Rect / Triangle(都会算面积)
定义"设备接口" Device → Printer / Scanner(都会 open()、close())
规定"算法骨架里的填空处" 父类写好流程,某一步交给子类(模板方法)

"抽象类"和"具体类"的分工:

抽象类 具体类
能创建对象吗 ❌ 不能 ✅ 能
有没有纯虚函数 有(至少一个) 没有
角色 定契约("必须会什么") 做实现("怎么做到")
位置 继承层次的上层 最下层(叶子)

"接口类"是一个常见特例:全是纯虚函数 + 一个虚析构,也就是"只有规格、一点实现都不给":

class IShape {
public:
    virtual double area() const = 0;
    virtual void   show() const = 0;
    virtual       ~IShape() {}          // 11.4.1 的铁律:有虚函数就要虚析构
};

四、动手 Demo(含实例08:创建纯虚函数)

#include <iostream>
using namespace std;

// ───── 抽象类:只规定"必须有 area() 和 name()",不管怎么实现 ─────
class Shape {
public:
    virtual double      area() const = 0;     // ★ 创建纯虚函数(实例08)
    virtual const char *name() const = 0;     // ★ 也是纯虚
    virtual void        show() const {        // 普通虚函数:有实现,子类可复用
        cout << name() << " 的面积 = " << area() << endl;
    }
    virtual ~Shape() { }
};

class Circle : public Shape {
private:
    double r;
public:
    Circle(double radius) : r(radius) {}
    double      area() const { return 3.14159 * r * r; }
    const char *name() const { return "圆"; }
};

class Square : public Shape {
private:
    double side;
public:
    Square(double s) : side(s) {}
    double      area() const { return side * side; }
    const char *name() const { return "正方形"; }
};

int main() {
    // Shape s;                    // ❌ 编译错:抽象类不能创建对象

    Shape *list[2];
    list[0] = new Circle(1);
    list[1] = new Square(3);

    for (int i = 0; i < 2; i++) {
        list[i]->show();           // 基类写好的 show(),靠虚函数各显神通
        delete list[i];            // 虚析构保证正确释放
    }
    return 0;
}

看到什么算成功:

圆 的面积 = 3.14159
正方形 的面积 = 9

读结果:

现象 说明
Shape s; 编译不过 抽象类不许创建对象——它"不完整"
Shape *list[2] 完全合法 指针/引用不受限制——这正是多态要用的形态(11.4.2)
show() 只在父类写了一次,两个子类各打各的 "不变的框架在父类、会变的部分交子类"——纯虚函数是这条分工的强制版
delete list[i] 正确释放 虚析构(11.4.1 的铁律,接口类里必写)

30 秒实验: 1. 把 Square 里的 const char *name() const { return "正方形"; } 注释掉。 2. 重新编译 → 报错:cannot allocate an object of abstract type 'Square'(因为那行 new Square(3)),并在下面列出一行 note:because the following virtual functions are pure within 'Square': virtual const char* Shape::name() const。 3. 看这一行 note——编译器直接告诉你"你欠哪个函数没实现"。 4. 取消注释,就好。 这个实验也说明第②条规则:少实现一个纯虚函数,子类自己也会变成抽象类。 5. 顺带试一次:如果在 main 里写 Square s(3);,报错会变成 cannot declare variable 's' to be of abstract type 'Square'——同一件事的两种说法,都指向"Square 现在还是抽象类"。

五、常见误区

  1. "= 0 是把函数赋值为 0"——不是。它是个标记,意思是"这个函数没有实现,子类必须给"。写成 = 0 的语法只对虚函数有效。
  2. "抽象类什么都不能用"——能用的多了:可以有数据成员、可以有普通成员函数、可以有带实现的普通虚函数、可以有构造函数(给子类用的)、可以有静态成员。唯一被禁的是"创建对象"。
  3. "纯虚函数必须先写 virtual"——对。= 0 不是独立语法,它是"虚函数"这个前提下的一种特殊形态。不写 virtual 直接 = 0 会报语法错。
  4. "抽象类的析构函数可以不虚"——必须虚(11.4.1)。抽象类的存在意义就是"被当父类用",那 delete 必然通过父类指针发生。接口类里"全纯虚 + 虚析构"是标配。
  5. "子类实现纯虚函数时要写 virtual"——可以不写(虚性遗传,11.4.1),但建议写 override(能帮你抓签名错误)。关键要求是"签名完全一致",差一个 const 就又变成隐藏了(11.4.1 的老坑)。
  6. "抽象类就是'没有数据的类'"——不是。"抽象"指的是"不完整、不能实例化",跟"有没有数据成员"无关。一个抽象类完全可以有大量数据成员(它只是"还有活没干完")。
自测三问(点开对答案) 1. 纯虚函数的语法是什么?含纯虚函数的类有什么硬限制?——语法:**`virtual 返回类型 函数名(参数) = 0;`**。硬限制:**① 不能创建对象**;**② 子类必须实现全部纯虚函数后才能创建对象**(少一个,子类也是抽象类)。**但可以定义指针和引用。** 2. 抽象类存在的意义是什么?——**描述"必须会做什么",不规定"怎么做"**——也就是**定义接口/契约**。它让"用父类指针统一处理一批子类对象"这件事(11.4.2 的动态绑定)有了**强制的约定**。 3. 抽象类的析构函数为什么要写成虚的?——因为**抽象类天生就是"被当父类用"的**,`delete` 一定通过父类指针发生;**析构不虚,子类的析构函数就不会执行**,子类申请的资源永久泄漏(11.4.1 的铁律)。

相关笔记