纯虚函数写法特殊: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 现在还是抽象类"。
五、常见误区
- "
= 0是把函数赋值为 0"——不是。它是个标记,意思是"这个函数没有实现,子类必须给"。写成= 0的语法只对虚函数有效。 - "抽象类什么都不能用"——能用的多了:可以有数据成员、可以有普通成员函数、可以有带实现的普通虚函数、可以有构造函数(给子类用的)、可以有静态成员。唯一被禁的是"创建对象"。
- "纯虚函数必须先写
virtual"——对。= 0不是独立语法,它是"虚函数"这个前提下的一种特殊形态。不写virtual直接= 0会报语法错。 - "抽象类的析构函数可以不虚"——必须虚(11.4.1)。抽象类的存在意义就是"被当父类用",那
delete必然通过父类指针发生。接口类里"全纯虚 + 虚析构"是标配。 - "子类实现纯虚函数时要写
virtual"——可以不写(虚性遗传,11.4.1),但建议写override(能帮你抓签名错误)。关键要求是"签名完全一致",差一个const就又变成隐藏了(11.4.1 的老坑)。 - "抽象类就是'没有数据的类'"——不是。"抽象"指的是"不完整、不能实例化",跟"有没有数据成员"无关。一个抽象类完全可以有大量数据成员(它只是"还有活没干完")。
自测三问(点开对答案)
1. 纯虚函数的语法是什么?含纯虚函数的类有什么硬限制?——语法:**`virtual 返回类型 函数名(参数) = 0;`**。硬限制:**① 不能创建对象**;**② 子类必须实现全部纯虚函数后才能创建对象**(少一个,子类也是抽象类)。**但可以定义指针和引用。** 2. 抽象类存在的意义是什么?——**描述"必须会做什么",不规定"怎么做"**——也就是**定义接口/契约**。它让"用父类指针统一处理一批子类对象"这件事(11.4.2 的动态绑定)有了**强制的约定**。 3. 抽象类的析构函数为什么要写成虚的?——因为**抽象类天生就是"被当父类用"的**,`delete` 一定通过父类指针发生;**析构不虚,子类的析构函数就不会执行**,子类申请的资源永久泄漏(11.4.1 的铁律)。- ☐ 11.6 小结(下一节)
相关笔记
- 利用虚函数实现动态绑定(含实例06) —— 上一节:父类里那个"保底实现"
- 小结 —— 下一节:整章知识地图与自测
- 虚函数概述 —— 虚函数与虚析构铁律
- 继承 —— is-a 关系与继承语法
- 面向对象的特点 —— 封装、继承、多态的合流处
- 🔷 C++基础 · 索引 —— 全课路线图