③ 面向对象程序设计 — 第 3 课
virtual,子类 override,要指针或引用。= 0 是抽象类;基类析构加 virtual。同样是 speak(),狗叫「汪汪」,猫叫「喵喵」。但我们希望调用它的代码写得一模一样:
p->speak(); // p 指向狗就是汪汪,指向猫就是喵喵
同一行代码,结果由 p 实际指向谁来决定。多态 = 同一个接口,不同行为。
它解决的是「扩展」问题:以后再加一只鸟,只要它也会 speak(),调用它的那个循环一个字都不用改。
virtual —— 让函数跟着实际对象走class Animal {
protected:
string name;
public:
Animal(string n) : name(n) {}
virtual void speak() { // 基类加 virtual
cout << name << " 叫了一声" << endl;
}
};
class Dog : public Animal {
public:
Dog(string n) : Animal(n) {}
void speak() override { // 子类加 override
cout << name << " 汪汪叫" << endl;
}
};
class Cat : public Animal {
public:
Cat(string n) : Animal(n) {}
void speak() override {
cout << name << " 喵喵叫" << endl;
}
};
virtual,子类写 override(表示「我在重写爸爸的函数」)。override 不是必须的,但强烈建议写:它会替你检查「是不是真的重写上了」。
Dog d("旺财");
Cat c("咪咪");
Animal* p = &d;
p->speak(); // 旺财 汪汪叫
p = &c;
p->speak(); // 咪咪 喵喵叫
只有通过基类指针/引用调用虚函数,才会在运行时判断真实类型。原因在于:p 本身只知道「我指着一个 Animal」,它需要多存一份信息才能知道「其实这是 Dog」。
Animal a = d; —— 这叫「对象切片」,Dog 多出来的部分被切掉,就只剩 Animal,多态失效。
切片之后,a 是一个货真价实的 Animal 对象,它身上已经没有 Dog 的那部分数据了。所以多态的第一道门槛是:用指针或引用,不要按值传递。
class Shape {
protected:
string name;
public:
Shape(string n) : name(n) {}
virtual double area() const = 0; // 纯虚函数:只声明,不实现
};
= 0 表示「每个子类必须自己实现 area()」。有纯虚函数的类叫抽象类,不能直接创建对象:Shape s("xxx"); ❌
抽象类的价值是「定规矩」:它规定所有图形都必须能算面积,但自己不提供算法。写 area() 的时候如果你忘了在子类实现,编译直接报错,而不是运行到一半才发现。
#include <iostream>
#include <string>
#include <vector>
using namespace std;
class Shape {
protected:
string name;
public:
Shape(string n) : name(n) {}
virtual double area() const = 0; // 每个图形自己算面积
virtual void show() const {
cout << name << " 面积 " << area() << endl;
}
};
class Circle : public Shape {
double r;
public:
Circle(string n, double radius) : Shape(n), r(radius) {}
double area() const override { return 3.14159 * r * r; }
};
class Rectangle : public Shape {
double w, h;
public:
Rectangle(string n, double a, double b) : Shape(n), w(a), h(b) {}
double area() const override { return w * h; }
};
int main() {
Circle c("圆", 3);
Rectangle r("矩形", 6, 6);
vector<Shape*> shapes = { &c, &r }; // 统一放基类指针
for (Shape* s : shapes) {
s->show(); // 自动调用对应的 area()
}
return 0;
}
关键在那句 s->show():同一个调用点,s 指向圆就调用 Circle::area,指向矩形就调用 Rectangle::area。以后加一个三角形,只要它实现了 area(),这个循环不用改一个字。
顺带一个细节:show() 也是 virtual,但它的子类没有重写它;这没问题,不重写就沿用父类的版本,里面调用的 area() 仍然会按实际类型走。
new 创建对象、通过基类指针 delete,基类析构函数要加 virtual。本课先知道有这个坑,后面用到再讲。
一句话解释为什么:delete p 时,如果析构不是虚函数,编译器只看 p 的静态类型(Animal),只调 ~Animal(),子类那部分的清理就漏掉了。
virtual:通过基类指针调用时,永远走基类版本,你那行子类实现形同虚设。Animal a = d; 触发对象切片,Dog 的部分被切掉,多态失效。const 不同,就没有重写成功。写 override 能让编译器当场报错,别省。Shape s("xxx"); 一定编译失败,因为纯虚函数没有实现。virtual:用基类指针 delete 时子类析构不执行,资源就漏了。Animal* p = &dog; 后 p->speak() 调用谁?Animal:virtual void speak()Dog / Cat:各自 override speak()vector<Animal*> 统一存起来speak(),要求输出狗的「汪汪」和猫的「喵喵」Bird(「叽叽」),不修改循环代码也能输出新动物的叫声。
Animal::speak 前面有没有 virtual?没有的话,循环里全是一个声音。override 还是把参数写歪了?vector 里存的是 Animal* 还是 Animal?存对象就会切片。virtual,子类 override;建议永远写 override。= 0,含它的类是抽象类,不能直接创建对象。vector<Shape*> 管理不同类型对象;基类析构要加 virtual。a + b、a == b、cout << a 认识你自己的类。为什么 operator<< 不能写成成员函数、必须请 friend 出马,下一课一次讲清。vector<Shape*> 循环,下一课会换成更顺手的写法:重载 << 之后,直接 cout << *s 就能打印。