C++: 多态
最后更新:2026-08-26
第31课我们学了继承——让派生类复用基类的代码。
但真实场景中,你需要用基类指针统一处理不同派生类的对象。
这就是多态——"一个接口,多种实现"。
1. 什么是多态?
(1) 1.1 生活中的多态
| 生活场景 | 程序中的对应 |
|---|---|
| 你对狗说"叫",它"汪汪" | 对基类指针调用 speak(),实际执行派生类的版本 |
| 你对猫说"叫",它"喵喵" | 同一个函数调用,不同对象有不同行为 |
多态的本质: 用基类指针指向派生类对象,调用虚函数时会动态绑定到派生类的实现。
(2) 1.2 为什么需要多态?
不用多态(麻烦):
▶ 示例 2:面向对象编程演示(难度⭐)
#include <iostream>
class Dog {
public:
void speak() { std::cout << "汪汪!" << std::endl; }
};
class Cat {
public:
void speak() { std::cout << "喵喵!" << std::endl; }
};
int main() {
Dog* d = new Dog();
Cat* c = new Cat();
d->speak();
c->speak();
// ❌ 要分别处理不同类型
return 0;
}
输出:
汪汪!
喵喵!
用多态(简洁):
#include <iostream>
class Animal {
public:
virtual void speak() { std::cout << "Animal speaks" << std::endl; }
};
class Dog : public Animal {
public:
void speak() override { std::cout << "汪汪!" << std::endl; }
};
class Cat : public Animal {
public:
void speak() override { std::cout << "喵喵!" << std::endl; }
};
int main() {
Animal* animals = {new Dog(), new Cat()};
for (int i = 0; i < 2; i++) {
animals[i]->speak(); // ✅ 多态:自动调用正确版本
}
return 0;
}
2. 虚函数(virtual)
(1) 2.1 基本用法
在基类里把函数声明为 virtual,派生类里override(重写)。
#include <iostream>
#include <string>
class Animal {
protected:
std::string name;
public:
Animal(const std::string& n) : name(n) {}
// 声明为虚函数
virtual void speak() {
std::cout << name << " makes a sound." << std::endl;
}
// 基类也要有虚析构函数!
virtual ~Animal() {}
};
class Dog : public Animal {
public:
Dog(const std::string& n) : Animal(n) {}
void speak() override {
std::cout << name << " says: 汪汪!" << std::endl;
}
};
int main() {
Animal* a = new Dog("Buddy");
a->speak(); // ✅ 输出:Buddy says: 汪汪!
delete a; // ✅ 会先调用 Dog 的析构函数,再调用 Animal 的
return 0;
}
⚠️ 重点: 基类有虚函数时,析构函数也要声明为 virtual!否则 delete 基类指针时不会调用派生类的析构函数。
(2) 2.2 override 关键字(C++11)
C++11 引入了 override 关键字,让编译器帮你检查是否真的重写了虚函数。
class Dog : public Animal {
public:
// ✅ 加了 override,编译器会检查
void speak() override { ... }
// ❌ 如果签名写错了(如 void speak(int x)),编译器会报错
// void speak(int x) override { ... } // 编译错误:没有可重写的函数
};
💡 建议: 永远加 override——让编译器帮你抓 bug。
3. 纯虚函数与抽象类
(1) 3.1 什么是纯虚函数?
纯虚函数是在基类里只声明不实现的虚函数,用 = 0 标记。
class Animal {
public:
// 纯虚函数
virtual void speak() = 0;
virtual ~Animal() {}
};
有纯虚函数的类叫"抽象类"——不能实例化,只能作为基类。
Animal a; // ❌ 编译错误:不能实例化抽象类
Animal* p = new Dog("Buddy"); // ✅ 可以用基类指针指向派生类
(2) 3.2 为什么需要纯虚函数?
迫使派生类必须重写这个函数。
class Animal {
public:
virtual void speak() = 0; // 强制派生类实现
};
class Dog : public Animal {
public:
void speak() override {
std::cout << "汪汪!" << std::endl;
}
// ✅ 如果忘记重写 speak(),编译器会报错
};
4. 虚函数的工作原理(选学)
(1) 4.1 虚函数表(vtable)
C++ 用虚函数表(vtable)实现多态:
- 有虚函数的类,编译器会为它生成一个
vtable(存虚函数地址的表) - 每个对象里有一个隐藏的
vptr(指向 vtable 的指针) - 调用虚函数时,通过
vptr找到正确的函数地址
💡 提示: 这就是为什么有虚函数的类,对象会多占一个指针的大小(64位系统上8字节)。
5. 实战:几何图形面积计算
▶ 示例 1:用多态计算不同图形的面积(难度⭐⭐)
#include <iostream>
#include <vector>
#include <cmath>
// 抽象基类
class Shape {
public:
virtual double area() = 0;
virtual void printInfo() = 0;
virtual ~Shape() {}
};
// 圆形
class Circle : public Shape {
private:
double radius;
public:
Circle(double r) : radius(r) {}
double area() override {
return 3.14159 * radius * radius;
}
void printInfo() override {
std::cout << "圆形,半径 = " << radius
<< ",面积 = " << area() << std::endl;
}
};
// 矩形
class Rectangle : public Shape {
private:
double width, height;
public:
Rectangle(double w, double h) : width(w), height(h) {}
double area() override {
return width * height;
}
void printInfo() override {
std::cout << "矩形," << width << " x " << height
<< ",面积 = " << area() << std::endl;
}
};
int main() {
std::vector<Shape*> shapes;
shapes.push_back(new Circle(5.0));
shapes.push_back(new Rectangle(4.0, 6.0));
for (Shape* s : shapes) {
s->printInfo();
}
// 释放内存
for (Shape* s : shapes) {
delete s;
}
return 0;
}
输出:
圆形,半径 =
矩形, x
❓ 常见问题
Q:为什么有虚函数,析构函数也要是虚的? A:如果你用
delete删除一个指向派生类对象的基类指针,而基类的析构函数不是虚的,那么只会调用基类的析构函数,派生类的资源会泄漏。CPPAnimal* a = new Dog("Buddy"); delete a; // ❌ 如果 ~Animal() 不是 virtual,Dog 的析构函数不会被调用黄金法则: 基类有虚函数,
~基类()一定要是virtual。
Q:override 和 final 有什么区别? A:> -
override:告诉编译器这个函数重写了基类的虚函数(让编译器帮你检查) > -final:告诉编译器这个函数不能被重写(用在派生类里)CPPclass Dog : public Animal { public: void speak() override final { ... } // final:派生类不能再重写 speak() };
Q:什么时候该用多态? A:当你需要用统一的接口处理不同类型的对象时。
典型场景:
- 游戏:所有角色继承自
Character,用Character*统一管理- GUI:所有控件继承自
Widget,用Widget*统一绘制- 文件系统:所有文件继承自
File,用File*统一读写
▶ 示例 3:虚函数多态(难度⭐)
#include <iostream>
#include <string>
class Shape {
public:
virtual double area() { return 0; }
virtual ~Shape() {}
};
class Circle : public Shape {
double radius;
public:
Circle(double r) : radius(r) {}
double area() override { return 3.14 * radius * radius; }
};
int main() {
Shape* s = new Circle(2.0);
std::cout << "面积:" << s->area() << std::endl;
delete s;
return 0;
}
输出:
面积:
virtual 声明虚函数,override 明确重写。基类指针调用派生类方法就是多态。
- 多态让基类指针能调用派生类的正确实现
- 基类里用
virtual声明虚函数 - 派生类里用
override重写(让编译器帮你检查) - 有虚函数的类,析构函数也要是
virtual - 纯虚函数(
= 0)迫使派生类必须重写 - 有纯虚函数的类是抽象类,不能实例化
📖 小节
- 多态:基类指针调用派生类方法
- virtual:声明虚函数
- override:明确表示重写基类方法
- 虚析构函数:确保正确释放派生类资源
📝 作业
-
基础题 (Difficulty ⭐): 定义一个
Vehicle基类,有纯虚函数void move()。让Car和Bicycle继承它,分别输出"汽车在马路上行驶"和"自行车在自行车道上骑行"。 -
进阶题 (Difficulty ⭐⭐): 扩展上面的程序,添加
void stop()虚函数,让Car输出"汽车靠边停车",Bicycle输出"自行车刹车"。 -
挑战题 (Difficulty ⭐⭐⭐): 设计一个员工薪资系统:
-
Employee抽象基类(姓名、工号、计算薪资virtual double calcSalary() = 0) -
FullTimeEmployee全职工(月薪) -
PartTimeEmployee兼职工(时薪 × 工时) -
用
vector<Employee*>存储所有员工,统一调用calcSalary() -
虚函数:基类声明,派生类重写,运行时多态
- virtual 关键字声明虚函数
- override 检查是否正确重写
- 纯虚函数和抽象类不能被实例化
- 虚函数表(vtable)实现运行时多态
6. 🚀 下一步
学会了多态,接下来我们学习 模板(Templates)(第33课)—— 让函数和类能支持任意类型,实现真正的"泛型编程"!