C++基础知识:虚函数和纯虚函数
完整总结虚函数和纯虚函数的用法、特点、适用场景和注意事项。
一、虚函数(virtual
)
特点
定义:
在基类中用
virtual
修饰的成员函数。子类可以选择重写(override),也可以不重写。
实现要求:
必须有函数体(实现),否则编译/链接会报错。
即便实现是空的
{}
,也要给。
多态:
通过基类指针或引用调用虚函数时,会发生动态绑定,实际执行的是子类的版本。
如果子类没重写,就用基类的实现。
适用场景
基类提供一个默认实现,子类可选是否覆盖。
比如:
class Shape { public:virtual void draw() { std::cout << "Default draw" << std::endl; } };
二、纯虚函数(= 0
)
特点
定义:
在基类中声明成
= 0
。语法:
virtual ReturnType func() = 0;
实现要求:
不需要基类实现(但也可以写实现,少见)。
子类 必须实现,否则子类仍然是抽象类,不能实例化。
抽象类:
含有至少一个纯虚函数的类是抽象类,不能直接实例化。
适用场景
只想定义一个接口/约束,不提供默认实现。
要求所有子类都必须实现自己的版本。
比如:
class Shape { public:virtual void draw() = 0; // 强制子类实现 };
三、对比总结
特性 | 普通虚函数 | 纯虚函数 |
---|---|---|
是否必须有实现 | ✅ 必须 | ❌ 不需要(可写也可不写) |
子类是否必须实现 | ❌ 可选 | ✅ 必须 |
基类能否实例化 | ✅ 可以 | ❌ 不行(抽象类) |
用途 | 提供默认行为,子类可选覆盖 | 定义接口规范,强制子类实现 |
四、注意事项
析构函数要声明为虚函数
如果基类会被继承,几乎总是应该写:
virtual ~Base() {}
防止通过基类指针释放子类对象时,子类析构函数不被调用,导致内存泄漏。
性能开销
虚函数通过 虚函数表(vtable) 实现,有很小的额外开销(一次间接寻址),通常可以忽略。
覆盖(override)要用关键字
C++11 以后推荐加
override
,防止拼写错误或签名不一致:void draw() override;
抽象类不能实例化
含有纯虚函数的类是抽象类,不能创建对象,只能通过指针/引用来使用多态。
接口类
如果一个类全是纯虚函数(且无成员变量),相当于 Java/C# 的“接口”。
五、什么时候用?
普通虚函数:
基类能给出一个合理的默认实现;
子类可选择是否覆盖。
例如:日志接口,默认
log()
输出到控制台,子类可重写输出到文件。
纯虚函数:
基类无法给出合理实现;
只想定义一个规范,强制所有子类实现。
例如:图形接口
draw()
,不同图形必须自己画。
✅ 一句话总结:
虚函数 = “有默认实现,子类可以改”。
纯虚函数 = “这是个接口,子类必须自己写”。