(六) 虚函数、重写与多态
把矩形和圆放到同一列表后,希望统一调用 area,却按实际图形采用不同公式。先沿这一调用观察,再解释 virtual 与 override。
运行时多态可以理解为:通过同一种基类接口操作不同派生对象,调用的实现由实际对象类型决定。
逐步看 shape->area() 怎样选择实现。 先创建具体的矩形和圆,再在 vector 中保存它们的 Shape* 地址,所以循环中的接口统一为 Shape。这些地址实际指向的对象分别是 Rectangle 和 Circle;area 被声明为虚函数,调用时会选择实际对象对应的实现。因此矩形 3×4 返回 12,半径为 1 的圆返回约 3.14159。
Shape::area() const = 0 说明基类只提出“图形应能求面积”这个接口,具体公式交给派生类。virtual ~Shape() = default; 让编译器提供这个虚析构函数的默认实现,并非“什么都不析构”。override 会检查派生类是否真正重写了该接口,例如漏写 const 就会报错。循环不需要再逐个判断“这是圆还是矩形”;新增一种正确实现接口的图形后,仍可使用同一个调用过程。
circle、rectangle 是 main 中的两个对象,shapes 保存它们的地址。循环取出地址后调用 area,执行的就是对应对象的面积计算;main 结束时,这两个局部对象随之销毁。
#include <iostream>
#include <vector>
using namespace std;
class Shape {
public:
virtual double area() const = 0; // 纯虚函数
virtual ~Shape() = default;
};
class Rectangle : public Shape {
private:
double width_;
double height_;
public:
Rectangle(double width, double height) : width_(width), height_(height) {}
double area() const override {
return width_ * height_;
}
};
class Circle : public Shape {
private:
double radius_;
public:
explicit Circle(double radius) : radius_(radius) {}
double area() const override {
constexpr double pi = 3.14159265358979323846;
return pi * radius_ * radius_;
}
};
int main() {
Rectangle rectangle(3, 4);
Circle circle(1);
vector<Shape *> shapes{&rectangle, &circle}; // 只保存有效地址,不拥有对象。
for (const auto &shape : shapes) {
cout << shape->area() << '\n';
}
}说明:
virtual声明虚函数,派生类提供对应实现。override让编译器检查重写是否正确,能发现参数或const不一致等问题。- 存在纯虚函数尚未实现的类是抽象类,不能直接创建该类对象。
- 通过基类指针或引用调用虚函数,可以保留运行时多态。
- 把派生对象按值复制成基类对象会发生对象切片,派生部分不会被保留。
基类的虚析构函数让销毁过程也能按实际对象类型进行:通过基类接口释放对象时,先执行派生类析构,再执行基类析构。上面的局部对象由所在作用域管理;动态对象的释放在下一节结合资源管理说明。参见 C++ 标准草案:delete 表达式。
函数重载依靠不同参数列表区分;虚函数重写依靠继承关系和匹配的函数签名。二者不是同一个概念。