C++: 多态

最后更新:2026-08-26

第31课我们学了继承——让派生类复用基类的代码。

但真实场景中,你需要用基类指针统一处理不同派生类的对象。

这就是多态——"一个接口,多种实现"。


1. 什么是多态?

(1) 1.1 生活中的多态

生活场景 程序中的对应
你对狗说"叫",它"汪汪" 对基类指针调用 speak(),实际执行派生类的版本
你对猫说"叫",它"喵喵" 同一个函数调用,不同对象有不同行为

多态的本质:基类指针指向派生类对象,调用虚函数时会动态绑定到派生类的实现。

(2) 1.2 为什么需要多态?

不用多态(麻烦):

▶ 示例 2:面向对象编程演示(难度⭐)

CPP
#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;
}
▶ 试一试

输出:

TEXT 📖 仅展示
汪汪!
喵喵!

用多态(简洁):

CPP
#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(重写)。

CPP
#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 关键字,让编译器帮你检查是否真的重写了虚函数。

CPP
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 标记。

CPP
class Animal {
public:
 // 纯虚函数
 virtual void speak() = 0;
 
 virtual ~Animal() {}
};

有纯虚函数的类叫"抽象类"——不能实例化,只能作为基类。

CPP
Animal a; // ❌ 编译错误:不能实例化抽象类
Animal* p = new Dog("Buddy"); // ✅ 可以用基类指针指向派生类

(2) 3.2 为什么需要纯虚函数?

迫使派生类必须重写这个函数。

CPP
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)实现多态:

  1. 有虚函数的类,编译器会为它生成一个 vtable(存虚函数地址的表)
  2. 每个对象里有一个隐藏的 vptr(指向 vtable 的指针)
  3. 调用虚函数时,通过 vptr 找到正确的函数地址

💡 提示: 这就是为什么有虚函数的类,对象会多占一个指针的大小(64位系统上8字节)。



5. 实战:几何图形面积计算

▶ 示例 1:用多态计算不同图形的面积(难度⭐⭐)

CPP 📖 仅展示
#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;
}
逻辑代码 44 行(超过 40 行限制,仅展示)

输出:

TEXT 📖 仅展示
圆形,半径 = 
矩形, x 

❓ 常见问题

Q:为什么有虚函数,析构函数也要是虚的? A:如果你用 delete 删除一个指向派生类对象的基类指针,而基类的析构函数不是虚的,那么只会调用基类的析构函数,派生类的资源会泄漏。

CPP
Animal* a = new Dog("Buddy");
delete a; // ❌ 如果 ~Animal() 不是 virtual,Dog 的析构函数不会被调用

黄金法则: 基类有虚函数,~基类() 一定要是 virtual

Q:override 和 final 有什么区别? A:> - override:告诉编译器这个函数重写了基类的虚函数(让编译器帮你检查) > - final:告诉编译器这个函数不能被重写(用在派生类里)

CPP
class Dog : public Animal {
public:
void speak() override final { ... } // final:派生类不能再重写 speak()
};

Q:什么时候该用多态? A:当你需要用统一的接口处理不同类型的对象时。

典型场景:

  • 游戏:所有角色继承自 Character,用 Character* 统一管理
  • GUI:所有控件继承自 Widget,用 Widget* 统一绘制
  • 文件系统:所有文件继承自 File,用 File* 统一读写

▶ 示例 3:虚函数多态(难度⭐)

CPP
#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;
}
▶ 试一试

输出:

TEXT 📖 仅展示
面积:
💡 提示virtual 声明虚函数,override 明确重写。基类指针调用派生类方法就是多态。



📖 小节

📝 作业

  1. 基础题 (Difficulty ⭐): 定义一个 Vehicle 基类,有纯虚函数 void move()。让 CarBicycle 继承它,分别输出"汽车在马路上行驶"和"自行车在自行车道上骑行"。

  2. 进阶题 (Difficulty ⭐⭐): 扩展上面的程序,添加 void stop() 虚函数,让 Car 输出"汽车靠边停车",Bicycle 输出"自行车刹车"。

  3. 挑战题 (Difficulty ⭐⭐⭐): 设计一个员工薪资系统

  4. Employee 抽象基类(姓名、工号、计算薪资 virtual double calcSalary() = 0

  5. FullTimeEmployee 全职工(月薪)

  6. PartTimeEmployee 兼职工(时薪 × 工时)

  7. vector<Employee*> 存储所有员工,统一调用 calcSalary()

  8. 虚函数:基类声明,派生类重写,运行时多态


6. 🚀 下一步

学会了多态,接下来我们学习 模板(Templates)(第33课)—— 让函数和类能支持任意类型,实现真正的"泛型编程"!

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏