C++ 多态(Polymorphism)深入解析
标签:C++ | 面向对象 | 多态 | 虚函数 | vtable | 抽象类
目标读者:已掌握 C++ 基础语法、类和继承概念的学习者
📋 摘要
本文系统讲解 C++ 面向对象编程的核心特性------多态(Polymorphism),从概念、实现条件到底层原理层层深入。内容涵盖:
- 多态的本质:同一接口,不同实现;运行时根据对象实际类型决定调用哪个函数
- 实现三要素:基类指针/引用 + 虚函数 + 函数重写,缺一不可
- 底层机制:虚函数表(vtable)与虚函数表指针(vptr),理解动态绑定的内存布局
- 抽象类与接口设计 :纯虚函数
= 0,抽象类不能实例化,强制派生类提供实现 - 常见陷阱:缺省参数不参与多态、忘记虚析构导致内存泄漏、对象切割(Slicing)
💡 核心记忆口诀:一虚二重写三指针,缺省参数不参与;抽象类来定接口,虚析构是必选项。
适合准备面试或巩固 OOP 基础的学习者阅读。
一、多态的概念
1.1 什么是多态?
多态(Polymorphism) 字面意思是"多种形态"。在 C++ 中,它指的是同一操作作用于不同对象时,可以有不同的解释,产生不同的执行结果。
💡 通俗比喻 :
想象一只"动物"在走动。你不知道它是猫还是狗,但可以统一说"动物在走"。
- 当它实际上是猫时,发出"喵~"的声音
- 当它实际上是狗时,发出"汪~"的声音
同一接口(walk),不同实现 ------ 这就是多态的魅力。
1.2 多态的分类
| 类型 | 别名 | 绑定时机 | 实现方式 |
|---|---|---|---|
| 编译时多态 | 静态多态 | 编译阶段 | 函数重载、运算符重载、模板 |
| 运行时多态 | 动态多态 | 运行阶段 | 虚函数 + 继承 + 基类指针/引用 |
⚠️ 核心要点 :本文重点讲解运行时多态,这是多态的核心应用场景。
二、运行时多态的实现条件
2.1 三个必要条件(缺一不可)
实现运行时多态必须同时满足以下三个条件:
cpp
// 条件一:必须有继承关系
class Base { ... };
class Derived : public Base { ... };
// 条件二:基类中必须声明虚函数
class Base {
public:
virtual void func(); // ← 必须有 virtual 关键字
};
// 条件三:派生类必须重写(override)基类虚函数
class Derived : public Base {
public:
void func() override; // ← 必须重写,签名完全相同
};
// 调用时必须通过基类指针或引用
Base* ptr = new Derived();
ptr->func(); // ← 必须用基类指针/引用作为媒介
2.2 条件详解
条件一:继承关系
多态建立在继承体系之上。没有继承,就没有"同一段代码处理不同类型对象"的基础。
条件二:虚函数(Virtual Function)
在成员函数前加 virtual 关键字声明的函数称为虚函数。
cpp
class Animal {
public:
virtual void speak() { // 虚函数
cout << "Animal makes sound" << endl;
}
};
虚函数也被称为 vtable 函数,因为它与虚函数表紧密相关。
条件三:函数重写(Override)
派生类中必须有一个与基类完全相同的虚函数(包括函数名、参数列表、返回值)。
📌 注意 :使用
override关键字可以明确标识这是重写,编译器会进行检查,避免拼写错误导致意外隐藏。
三、虚函数的重写(Override)
3.1 重写的规则
| 项目 | 要求 |
|---|---|
| 函数名 | 必须相同 |
| 参数列表 | 必须相同(类型、个数、顺序) |
| 返回值 | 必须相同(协变除外) |
| const 修饰 | 必须一致 |
cpp
class Base {
public:
virtual void func(int x) const; // 基类虚函数
};
class Derived : public Base {
public:
// ✅ 正确重写
void func(int x) const override;
// ❌ 错误:参数不同,构成隐藏而非重写
void func(double x) override;
// ❌ 错误:缺少 const,构成隐藏
void func(int x) override;
};
3.2 协变(Covariance)------ 特殊例外
协变 是指:派生类重写基类虚函数时,返回值可以是基类指针/引用的派生类指针/引用。
cpp
class Base {
public:
virtual Base* clone() const;
};
class Derived : public Base {
public:
// ✅ 协变:返回 Derived*,是 Base* 的派生类指针
Derived* clone() const override;
};
📝 说明:协变在笔试中考得较少,理解即可。实际工程中很少见到这种写法。
3.3 构造函数不能是虚函数
cpp
class Base {
public:
// ❌ 错误!构造函数不能是虚函数
virtual Base() {}
};
原因:构造对象时,虚函数表指针(vptr)尚未初始化,此时调用虚函数没有意义。
3.4 析构函数建议声明为虚函数
cpp
class Base {
public:
virtual ~Base() { // ✅ 推荐:虚析构函数
cout << "Base destructor" << endl;
}
};
class Derived : public Base {
public:
~Derived() override {
cout << "Derived destructor" << endl;
}
};
int main() {
Base* ptr = new Derived();
delete ptr; // 正确调用 Derived 和 Base 的析构函数
// 如果 Base 析构函数不是虚函数,只会调用 Base::~Base()
// 导致 Derived 的资源泄漏!
return 0;
}
⚠️ 重要规则 :任何可能被多态使用的类,析构函数必须声明为 virtual!
四、多态调用与动态绑定
4.1 多态调用的形式
cpp
class Animal {
public:
virtual void speak() {
cout << "Animal makes sound" << endl;
}
};
class Dog : public Animal {
public:
void speak() override {
cout << "Dog barks: Woof!" << endl;
}
};
class Cat : public Animal {
public:
void speak() override {
cout << "Cat meows: Meow!" << endl;
}
};
int main() {
Animal* ptr1 = new Dog();
Animal* ptr2 = new Cat();
ptr1->speak(); // 输出:Dog barks: Woof!
ptr2->speak(); // 输出:Cat meows: Meow!
// 也可以通过引用调用
Animal& ref = *ptr1;
ref.speak(); // 输出:Dog barks: Woof!
delete ptr1;
delete ptr2;
return 0;
}
4.2 动态绑定 vs 静态绑定
| 绑定类型 | 别名 | 发生时机 | 适用场景 |
|---|---|---|---|
| 动态绑定 | 晚期绑定 | 运行时 | 虚函数通过指针/引用调用 |
| 静态绑定 | 早期绑定 | 编译时 | 普通函数、非虚函数 |
cpp
Animal a;
Dog d;
a.speak(); // 静态绑定:编译时就确定调用 Animal::speak()
d.speak(); // 静态绑定:编译时就确定调用 Dog::speak()
Animal* ptr = &d;
ptr->speak(); // 动态绑定:运行时根据 ptr 实际指向的对象决定调用哪个函数
4.3 缺省参数与多态的陷阱
cpp
class Base {
public:
virtual void func(int x = 10) {
cout << "Base: " << x << endl;
}
};
class Derived : public Base {
public:
void func(int x = 20) override { // ❌ 不推荐!
cout << "Derived: " << x << endl;
}
};
int main() {
Base* ptr = new Derived();
ptr->func(); // 输出:Derived: 10(缺省参数取基类的值!)
return 0;
}
⚠️ 重要规则 :不要在派生类中重新定义继承下来的缺省参数值!
原因:缺省参数在编译时 确定,而虚函数调用在运行时确定,两者结合会产生不可预期的行为。
五、虚函数表(vtable)与内存布局
5.1 虚函数表的概念
每个包含虚函数的类都有一个虚函数表(Virtual Table,简称 vtable),表中存储了该类所有虚函数的地址。
Base 类的虚函数表:
┌─────────────────────────┐
│ 偏移 0: 指向 Base::func │ ← Base::func 的地址
│ 偏移 1: 指向 Base::~Base│ ← Base 析构函数地址
└─────────────────────────┘
Derived 类的虚函数表:
┌─────────────────────────┐
│ 偏移 0: 指向 Derived::func │ ← 被重写,指向 Derived 版本
│ 偏移 1: 指向 Derived::~Derived│ ← 派生类析构
└─────────────────────────┘
5.2 虚函数表指针(vptr)
每个对象内部都隐藏着一个虚函数表指针(vptr),指向其所属类的虚函数表。
Base 对象 b1 的内存布局:
┌──────────────┬──────────────┐
│ vptr │ Base成员数据 │
│ → 0x00401500 │ │
└──────────────┴──────────────┘
↓
vtable_Base
Derived 对象 d1 的内存布局:
┌──────────────┬──────────────┬──────────────┐
│ vptr │ Base成员数据 │ Derived成员数据 │
│ → 0x00401504 │ │ │
└──────────────┴──────────────┴──────────────┘
↓
vtable_Derived
关键结论:
- 每个对象有自己独立的 vptr
- 同一类的所有对象共享同一个 vtable
- vptr 在对象构造时由编译器插入代码初始化
5.3 多态调用过程(底层原理)
cpp
Base* pb = new Derived();
pb->func();
底层执行过程:
1. 编译器生成代码:通过 pb 访问对象的 vptr
vptr = *(void**)(pb)
2. 通过 vptr 找到对应的虚函数表
vtable = *vptr
3. 在虚函数表中查找 func 的地址
func_addr = vtable[0] // 假设 func 在偏移 0 处
4. 跳转到 func_addr 执行
((void(*)())func_addr)()
5.4 单继承的虚函数表
class Base {
public:
virtual void func1();
virtual void func2();
virtual ~Base();
};
class Derived : public Base {
public:
void func1() override; // 重写 func1
void func3(); // 新增虚函数
};
内存布局示意:
Base 对象:
┌──────────────┬─────────────────┐
│ vptr │ Base 数据成员 │
│ → vtable_B │ │
└──────────────┴─────────────────┘
Derived 对象:
┌──────────────┬─────────────────┬─────────────────┐
│ vptr │ Base 数据成员 │ Derived 数据成员 │
│ → vtable_D │ │ │
└──────────────┴─────────────────┴─────────────────┘
vtable_Base: [func1@Base] [func2@Base] [dtor@Base]
vtable_Derived: [func1@Derived] [func2@Base] [dtor@Derived] [func3@Derived]
六、纯虚函数与抽象类
6.1 纯虚函数
纯虚函数 是在基类中只声明、不实现的虚函数,用 = 0 表示。
cpp
class Shape {
public:
virtual double area() const = 0; // 纯虚函数
virtual void draw() const = 0; // 纯虚函数
};
6.2 抽象类
抽象类是包含至少一个纯虚函数的类。
抽象类的特点:
| 特性 | 说明 |
|---|---|
| 不能实例化 | Shape s; 编译报错 |
| 可以作为指针/引用类型 | Shape* ptr; 合法 |
| 派生类必须实现所有纯虚函数 | 否则派生类也是抽象类 |
cpp
class Circle : public Shape {
public:
double area() const override { return 3.14 * r * r; }
void draw() const override { cout << "Drawing circle" << endl; }
private:
double r;
};
// Shape s; // ❌ 错误!不能实例化抽象类
Circle c; // ✅ 正确
c.area(); // ✅ 正确
6.3 抽象类的实际应用
cpp
// 定义图形接口
class Shape {
public:
virtual double area() const = 0;
virtual void draw() const = 0;
virtual ~Shape() = default;
};
// 具体实现
class Circle : public Shape {
public:
Circle(double r) : r_(r) {}
double area() const override { return 3.14 * r_ * r_; }
void draw() const override { cout << "Drawing circle" << endl; }
private:
double r_;
};
class Rectangle : public Shape {
public:
Rectangle(double w, double h) : w_(w), h_(h) {}
double area() const override { return w_ * h_; }
void draw() const override { cout << "Drawing rectangle" << endl; }
private:
double w_, h_;
};
// 多态使用
void printArea(const Shape& shape) {
cout << "Area: " << shape.area() << endl;
}
int main() {
Circle c(5.0);
Rectangle r(3.0, 4.0);
printArea(c); // 输出:Area: 78.5
printArea(r); // 输出:Area: 12
// 可以用基类指针数组管理不同类型的对象
Shape* shapes[] = { &c, &r };
for (auto s : shapes) {
s->draw();
cout << "Area: " << s->area() << endl;
}
return 0;
}
七、多态的综合示例
7.1 完整的继承体系示例
cpp
#include <iostream>
#include <string>
using namespace std;
// 基类:动物
class Animal {
public:
Animal(const string& name) : name_(name) {
cout << "Animal constructor: " << name_ << endl;
}
virtual ~Animal() {
cout << "Animal destructor: " << name_ << endl;
}
// 虚函数:发出声音
virtual void speak() const {
cout << name_ << " makes sound" << endl;
}
// 普通函数(非虚)
void sleep() const {
cout << name_ << " is sleeping" << endl;
}
protected:
string name_;
};
// 派生类:狗
class Dog : public Animal {
public:
Dog(const string& name, const string& breed)
: Animal(name), breed_(breed) {
cout << "Dog constructor: " << name << endl;
}
~Dog() override {
cout << "Dog destructor: " << name_ << endl;
}
void speak() const override {
cout << name_ << " barks: Woof!" << endl;
}
void fetch() const {
cout << name_ << " is fetching" << endl;
}
private:
string breed_;
};
// 派生类:猫
class Cat : public Animal {
public:
Cat(const string& name, bool indoor)
: Animal(name), indoor_(indoor) {
cout << "Cat constructor: " << name << endl;
}
~Cat() override {
cout << "Cat destructor: " << name_ << endl;
}
void speak() const override {
cout << name_ << " meows: Meow!" << endl;
}
void climb() const {
cout << name_ << " is climbing" << endl;
}
private:
bool indoor_;
};
int main() {
// 多态:通过基类指针/引用调用
Animal* animals[] = {
new Dog("旺财", "金毛"),
new Cat("咪咪", true)
};
for (auto a : animals) {
a->speak(); // 多态调用:根据实际类型决定
a->sleep(); // 静态绑定:始终调用 Animal::sleep()
}
// 清理
for (auto a : animals) {
delete a; // 虚析构:正确调用派生类析构
}
return 0;
}
输出结果:
Animal constructor: 旺财
Dog constructor: 旺财
Animal constructor: 咪咪
Cat constructor: 咪咪
旺财 barks: Woof!
咪咪 meows: Meow!
旺财 is sleeping
咪咪 is sleeping
Cat destructor: 咪咪
Animal destructor: 咪咪
Dog destructor: 旺财
Animal destructor: 旺财
7.2 纯虚函数与接口设计
cpp
// 抽象基类:可攻击单位
class Attackable {
public:
virtual void attack() const = 0; // 纯虚函数
virtual void defend() const = 0; // 纯虚函数
virtual ~Attackable() = default;
};
// 战士实现
class Warrior : public Attackable {
public:
void attack() const override {
cout << "Warrior attacks with sword!" << endl;
}
void defend() const override {
cout << "Warrior defends with shield!" << endl;
}
};
// 法师实现
class Mage : public Attackable {
public:
void attack() const override {
cout << "Mage casts fireball!" << endl;
}
void defend() const override {
cout << "Mage casts shield spell!" << endl;
}
};
// 统一接口使用
void battle(Attackable& a1, Attackable& a2) {
a1.attack();
a2.defend();
}
八、常见问题与注意事项
8.1 多态触发的三个条件
| 条件 | 说明 | 违反后果 |
|---|---|---|
| 基类指针/引用 | 必须通过指针或引用调用 | 静态绑定,无多态效果 |
| 虚函数 | 基类函数必须声明为 virtual | 非虚函数始终静态绑定 |
| 函数重写 | 派生类必须重写虚函数 | 调用基类版本 |
8.2 内存开销
| 项目 | 开销 |
|---|---|
| 虚函数表 | 每个类一张,占用静态存储区 |
| vptr | 每个对象增加一个指针大小(32位系统4字节,64位系统8字节) |
| 动态绑定 | 运行时查表,有轻微性能开销 |
8.3 常见错误
cpp
// 错误1:忘记虚析构函数
class Base {
public:
~Base() {} // ❌ 非虚析构
};
class Derived : public Base {
int* data;
~Derived() { delete[] data; } // ❌ 通过 Base* 删除时不会调用
};
// 错误2:通过对象调用"多态"
Base obj;
Derived d;
obj = d; // ❌ 对象切割(Slicing),丢失派生类信息
// 错误3:隐式转换导致非多态调用
Base b = Derived(); // ❌ 对象切割
b.func(); // 始终调用 Base::func()
九、知识总结
9.1 核心要点
多态 = 继承 + 虚函数 + 重写 + 基类指针/引用调用
9.2 记忆口诀
一虚二重写三指针,缺省参数不参与;
抽象类来定接口,虚析构是必选项。
9.3 关键对比表
| 概念 | 定义 | 特点 |
|---|---|---|
| 虚函数 | 加 virtual 的成员函数 |
支持动态绑定 |
| 纯虚函数 | virtual ... = 0 |
无实现,强制子类重写 |
| 抽象类 | 含纯虚函数的类 | 不能实例化 |
| 重写 | 派生类与基类虚函数签名完全相同 | 实现多态的关键 |
| 重载 | 同一作用域内函数名相同参数不同 | 编译时多态 |
| 隐藏 | 派生类屏蔽基类同名成员 | 非虚函数行为 |
十、复习挑战清单
看完本文后,尝试回答以下问题:
- 说出实现多态的三个必要条件
- 解释为什么构造函数不能是虚函数
- 说明虚函数表和 vptr 的关系
- 画出单继承下派生类对象的内存布局
- 解释动态绑定和静态绑定的区别
- 为什么抽象类不能实例化?
- 什么情况下必须使用虚析构函数?
- 缺省参数和多态结合会有什么问题?
- 手写一个包含继承、虚函数、多态调用的完整示例
- 解释"对象切割"(Slicing)现象及其原因
十一、延伸阅读
- [C++ 虚函数表(vtable)深度剖析](#C++ 虚函数表(vtable)深度剖析)
- [C++ 智能指针与多态:unique_ptr、shared_ptr](#C++ 智能指针与多态:unique_ptr、shared_ptr)
- [C++11 新特性:override、final、= default、= delete](#C++11 新特性:override、final、= default、= delete)
- 设计模式:策略模式与多态
- [C++ 运行时类型识别(RTTI)与 dynamic_cast](#C++ 运行时类型识别(RTTI)与 dynamic_cast)
参考代码索引
| 文件 | 内容 |
|---|---|
polymorphism_basic.cpp |
虚函数与多态基础示例 |
polymorphism_override.cpp |
函数重写与协变示例 |
polymorphism_abstract.cpp |
纯虚函数与抽象类示例 |
polymorphism_vtable.cpp |
虚函数表内存布局演示 |
polymorphism_interface.cpp |
纯虚函数实现接口设计 |
polymorphism_slicing.cpp |
对象切割问题演示 |
任何批评和建议欢迎指出,我们共同进步!
本文原创,欢迎分享,转载请标明出处。