C++ 多态详解(从基础到工程实践)
多态(Polymorphism)是面向对象编程的三大特性之一,在 C++ 中主要通过虚函数(virtual function)实现,是 C++ 中最强大也最容易出错的特性之一。
下面从原理 → 用法 → 细节 → 常见陷阱 → 工程实践,一次性把 C++ 多态讲透。
1. 多态的核心概念
C++ 中的多态指的是:
“同一个接口,不同实现”
通过基类指针或引用调用虚函数时,实际执行的是派生类的版本,而不是基类的版本。
一句话总结:
“父类指针(或引用)指向子类对象时,通过虚函数调用子类的实现”
2. 多态的实现方式(最核心的三种)
方式一:虚函数 + 指针/引用(最常用)
#include <iostream>
using namespace std;
class Animal {
public:
// 声明为虚函数
virtual void speak() const {
cout << "Animal is speaking...\n";
}
virtual ~Animal() = default; // 虚析构函数(非常重要!)
};
class Dog : public Animal {
public:
void speak() const override { // override 是 C++11 推荐写法
cout << "Woof! Woof!\n";
}
};
class Cat : public Animal {
public:
void speak() const override {
cout << "Meow~\n";
}
};
int main() {
Animal* animal1 = new Dog();
Animal* animal2 = new Cat();
animal1->speak(); // 输出:Woof! Woof!
animal2->speak(); // 输出:Meow~
delete animal1;
delete animal2;
return 0;
}
关键点:
virtual只能在基类中首次声明- 子类可以不写
virtual,但强烈建议写override(编译器会检查是否真的重写) - 必须用指针或引用才能实现多态(值传递会切片!)
方式二:虚函数表(vtable)原理(面试常考)
C++ 通过虚函数表(vtable)实现多态。
- 每个有虚函数的类都有一个隐藏的虚函数表指针(vptr)
- vptr 指向类的虚函数表(vtable)
- 虚函数表是一个函数指针数组,存储该类的所有虚函数地址
- 子类会复制并修改父类的虚函数表(覆盖对应位置)
内存布局示意(Dog 对象):
Dog 对象内存:
[ vptr ] → 指向 Dog 的 vtable
[ 其他成员 ]
Dog 的 vtable:
[ &Dog::speak ]
[ &Animal::~Animal ] // 虚析构函数
...
3. 虚函数 vs 普通函数(对比表)
| 特性 | 普通成员函数 | 虚函数 (virtual) |
|---|---|---|
| 是否多态 | 否 | 是 |
| 调用方式 | 静态绑定(编译期确定) | 动态绑定(运行期通过 vptr) |
| 开销 | 几乎无 | 一次虚表指针查找 + 间接调用 |
| 是否可以被 override | 否 | 是 |
| 析构函数 | 一般不虚 | 必须虚(否则内存泄漏风险) |
4. 虚析构函数(最重要、最常犯错的点)
永远记住:只要一个类可能被当作基类使用,就要把析构函数写成虚函数!
class Base {
public:
// 非虚析构 → 大坑!
// ~Base() { cout << "Base dtor\n"; }
virtual ~Base() { cout << "Base dtor\n"; }
};
class Derived : public Base {
public:
~Derived() { cout << "Derived dtor\n"; }
};
int main() {
Base* p = new Derived();
delete p; // 如果 ~Base() 非虚,只调用 Base 析构 → 内存泄漏
// 如果是虚析构 → 先 Derived → 再 Base
}
结论:
- 基类必须有虚析构函数(除非你明确知道这个类永远不会被 delete 通过基类指针)
- 虚析构函数会让类产生 vptr(虚表指针),增加 8 字节(64 位系统)
5. 常见误区与陷阱
- 用值传递实现不了多态(切片问题)
void makeSound(Animal animal) { // 值传递 → 切片
animal.speak(); // 永远调用 Animal::speak()
}
正确写法:
void makeSound(const Animal& animal) { // 引用或指针
animal.speak();
}
- override 与 final(C++11+ 强烈推荐)
class Base {
public:
virtual void func() const = 0; // 纯虚函数
};
class Derived : public Base {
public:
void func() const override final { // 强制重写 + 禁止再被重写
cout << "Derived func\n";
}
};
- 不要在构造函数/析构函数中调用虚函数
class Base {
public:
Base() { who(); } // 危险!
virtual void who() { cout << "Base\n"; }
};
class Derived : public Base {
public:
void who() override { cout << "Derived\n"; }
};
→ 构造 Base 时,Derived 还没构造完成,who() 调用的是 Base 版本
- 虚函数默认参数是静态绑定
virtual void print(int x = 10);
→ 调用时用的是声明处的默认参数,不是实际类型的
6. 多态的工程实践建议(2025–2026)
- 基类析构函数一律写 virtual(除非你明确知道不会通过基类指针 delete)
- 尽量用 override 和 final(编译期检查,防止笔误)
- 优先用 const 引用传参(
const Animal&) - 接口类用纯虚函数(=0),让子类必须实现
- 避免在基类中放太多数据(虚函数表 + 数据成员会增加对象体积)
- 需要运行时类型信息 → 用
dynamic_cast(有开销) - 性能敏感场景 → 考虑 CRTP(静态多态)替代运行时多态
7. 快速记忆口诀
- 多态 = virtual + 指针/引用 + 运行时
- 基类必须有虚析构
- 子类重写用 override,禁止子类再重写用 final
- 不要值传递,不要在构造/析构中调用虚函数
- 接口类 → 纯虚函数(=0)
想深入哪一部分?
比如:
- 虚函数表内存布局手撕
- 多态 + 模板(CRTP 静态多态)
- dynamic_cast / typeid 用法
- 纯虚函数 + 抽象类设计
- 多重继承下的虚函数表
告诉我具体想看哪一块,我可以继续展开代码和图解。