目录
[1. 多态的概念](#1. 多态的概念)
[2. 多态的定义及实现](#2. 多态的定义及实现)
[2.1 多态的构成条件](#2.1 多态的构成条件)
[2.2 虚函数](#2.2 虚函数)
[2.3 虚函数的重写](#2.3 虚函数的重写)
[2.4 虚函数重写的两个例外](#2.4 虚函数重写的两个例外)
[(1) 协变 (Covariance)](#(1) 协变 (Covariance))
[(2) 析构函数的重写](#(2) 析构函数的重写)
[2.5 C++11 override 和 final](#2.5 C++11 override 和 final)
[2.6 重载、覆盖(重写)、隐藏(重定义)的对比](#2.6 重载、覆盖(重写)、隐藏(重定义)的对比)
[3. 抽象类](#3. 抽象类)
[3.1 概念](#3.1 概念)
[3.2 接口继承和实现继承](#3.2 接口继承和实现继承)
[4. 多态的原理](#4. 多态的原理)
[4.1 虚函数表](#4.1 虚函数表)
[4.2 多态的原理](#4.2 多态的原理)
[4.3 动态绑定与静态绑定](#4.3 动态绑定与静态绑定)
[5. 单继承和多继承关系中的虚函数表](#5. 单继承和多继承关系中的虚函数表)
[5.1 单继承中的虚函数表](#5.1 单继承中的虚函数表)
[5.2 多继承中的虚函数表](#5.2 多继承中的虚函数表)
[6. 继承和多态常见的面试问题](#6. 继承和多态常见的面试问题)
[7. 总结](#7. 总结)
前言
在 C++ 面向对象编程中,多态(Polymorphism) 是继封装和继承之后的第三大核心特性。通俗来说,多态就是"多种形态",即去完成某个行为,当不同的对象去完成时会产生出不同的状态。
本文将严格按照定义、用法、原理及面试高频考点的逻辑,带你彻底搞懂 C++ 多态。
1. 多态的概念
多态分为静态多态 和动态多态:
静态多态 :在编译期间确定,主要通过函数重载 和模板实现。
动态多态 :在运行期间确定,主要通过继承 和虚函数实现。
本文主要讨论的是动态多态。
生活中的例子: 比如买票这个行为。普通成年人买票是全价,学生买票是半价,军人买票是优先通道。同一个"买票"的动作,不同的人(对象)去执行,产生了不同的结果。
2. 多态的定义及实现
2.1 多态的构成条件
在继承体系中,实现多态必须同时满足以下两个严格条件:
必须通过基类的指针或者引用调用虚函数。
被调用的函数必须是虚函数 ,且派生类必须对该虚函数进行重写(覆盖)
2.2 虚函数
被 virtual 关键字修饰的成员函数称为虚函数。
cpp
class Person {
public:
virtual void BuyTicket() {
cout << "买票-全价" << endl;
}
};
2.3 虚函数的重写
派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名、参数列表完全相同),称派生类的虚函数重写了基类的虚函数。
cpp
#include <iostream>
using namespace std;
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
// 派生类重写基类虚函数
virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
class Soldier : public Person {
public:
virtual void BuyTicket() { cout << "买票-优先" << endl; }
};
// 多态调用:传什么对象,就调什么对象的函数
void Pay(Person& p) {
p.BuyTicket();
}
int main() {
Person ps;
Student st;
Soldier so;
Pay(ps); // 输出:买票-全价
Pay(st); // 输出:买票-半价
Pay(so); // 输出:买票-优先
return 0;
}
2.4 虚函数重写的两个例外
虽然重写要求返回值、函数名、参数列表都相同,但有两个特殊情况:
(1) 协变 (Covariance)
基类与派生类虚函数返回值类型不同,但必须是父子关系的指针或引用。 即:基类虚函数返回基类对象的指针/引用,派生类虚函数返回派生类对象的指针/引用。
cpp
class A {};
class B : public A {};
class Person {
public:
virtual A* f() { return new A; }
};
class Student : public Person {
public:
// 返回值类型不同(A* vs B*),但构成协变,依然是重写
virtual B* f() { return new B; }
};
(2) 析构函数的重写
如果基类的析构函数为虚函数,此时派生类只要定义析构函数,无论是否加 virtual 关键字,都与基类析构函数构成重写。
-
原因 :编译器在编译后会将析构函数的名称统一处理成
destructor,目的是为了构成多态,正确释放内存。
重要场景:
cpp
class Person {
public:
virtual ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:
~Student() { cout << "~Student()" << endl; }
};
int main() {
// 如果析构函数不是虚函数,delete p 时只会调 Person 的析构,导致内存泄漏
Person* p = new Student;
delete p; // 多态调用:先调 ~Student(),再调 ~Person()
return 0;
}
2.5 C++11 override 和 final
C++11 提供了两个关键字来帮助我们在编译阶段检查多态的正确性:
final:修饰虚函数,表示该虚函数不能再被重写。
override:检查派生类虚函数是否重写了基类的某个虚函数,如果没有重写(比如拼写错误),编译器报错。
cpp
class Car {
public:
virtual void Drive() {}
};
class Benz : public Car {
public:
virtual void Drive() override { cout << "Benz" << endl; } // 检查是否重写成功
};
2.6 重载、覆盖(重写)、隐藏(重定义)的对比
这是一个非常容易混淆的概念,总结如下表:
|---------|---------------|------------------|-----------------|
| 特性 | 重载 (Overload) | 覆盖/重写 (Override) | 隐藏/重定义 (Hiding) |
| 作用域 | 在同一个作用域 | 分别在基类和派生类 | 分别在基类和派生类 |
| 函数名 | 相同 | 相同 | 相同 |
| 参数列表 | 必须不同 | 必须相同 | 只要函数名相同,非重写即隐藏 |
| 返回值 | 无要求 | 要求相同(协变除外) | 无要求 |
| virtual | 无要求 | 基类必须有 virtual | 无要求 |
3. 抽象类
3.1 概念
在虚函数的后面写上 =0 ,则这个函数为纯虚函数 。 包含纯虚函数的类叫做抽象类 (也叫接口类)。抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化
cpp
class Car {
public:
// 纯虚函数
virtual void Drive() = 0;
};
class BMW : public Car {
public:
virtual void Drive() {
cout << "BMW-操控" << endl;
}
};
int main() {
// Car c; // 错误,抽象类不能实例化
Car* p = new BMW;
p->Drive();
return 0;
}
3.2 接口继承和实现继承
普通函数的继承 是一种实现继承,派生类继承了基类函数的实现,可以使用该函数。
虚函数的继承 是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态。
如果不实现多态,不要把函数定义成虚函数。
4. 多态的原理
4.1 虚函数表
我们先看一个问题:sizeof(Base) 是多少?
cpp
class Base {
public:
virtual void Func1() { cout << "Func1" << endl; }
private:
int _b = 1;
};
在 32 位系统下,结果是 8 字节 。除了 int _b 占 4 字节外,还有一个 4 字节的指针,我们称之为虚函数表指针 (vptr)。
一个含有虚函数的类中至少有一个虚函数表指针,该指针指向一个数组,数组中存放的是虚函数的地址,这个数组叫做虚函数表 (vftable)。
虚函数表本质是一个函数指针数组,一般以 nullptr 结尾。

4.2 多态的原理
多态是如何实现的?
编译期间:编译器检查代码,如果满足多态的两个条件(指针/引用调用 + 虚函数),则生成特殊的指令。
运行期间:
当
p->BuyTicket()被调用时,程序并不知道p指向的是Person对象还是Student对象。程序会通过
p指针找到对象内存中的 vptr。通过 vptr 找到对应的 虚函数表。
在虚函数表中找到
BuyTicket的实际地址并调用。
总结 :如果对象是 Student,vptr 指向 Student 的虚表(其中包含重写后的 Student::BuyTicket 地址);如果对象是 Person,vptr 指向 Person 的虚表。这就是"动态绑定"。
4.3 动态绑定与静态绑定
静态绑定:在编译阶段就确定了函数的地址(如普通函数调用、函数重载)。
动态绑定:在程序运行期间,根据具体拿到的对象类型确定程序的具体行为,调用具体的函数(多态)。
5. 单继承和多继承关系中的虚函数表
5.1 单继承中的虚函数表
在单继承中,派生类的虚函数表生成规则如下:
先将基类的虚表内容拷贝一份到派生类虚表中。
如果派生类重写了基类的某个虚函数,则用派生类自己的虚函数地址覆盖虚表中基类的虚函数地址。
派生类自己新增加的虚函数,按其在派生类中的声明次序,增加到派生类虚表的最后。

5.2 多继承中的虚函数表
如果一个类继承了两个带有虚函数的基类:

Derive对象中会有两个 vptr。
Base1的 vptr 指向第一张虚表,Base2的 vptr 指向第二张虚表。派生类重写了
func1,则第一张虚表中的func1被覆盖。注意 :派生类自己新增的虚函数(如
func3),通常会添加在第一个继承基类(Base1)的虚表后面。
6. 继承和多态常见的面试问题
Q1: inline 函数可以是虚函数吗?
- 答 :可以,但有前提。如果是普通调用,它依然可以被内联;如果是多态调用 ,编译器会忽略
inline属性,因为多态需要在运行时去虚表中找地址,而内联是在编译时展开,两者冲突。
Q2: 静态成员函数可以是虚函数吗?
- 答 :不能。静态成员函数没有
this指针,而虚函数的调用依赖this指针找到 vptr。
Q3: 构造函数可以是虚函数吗?
- 答:不能。虚函数表指针 (vptr) 是在构造函数初始化列表阶段才初始化的。如果构造函数是虚函数,调用它需要查虚表,但此时虚表指针还没初始化,形成悖论。
Q4: 析构函数建议设为虚函数吗?
-
答:强烈建议。如果用基类指针指向派生类对象,且基类析构函数不是虚函数,delete 时只会调用基类的析构,导致派生类资源未释放(内存泄漏)。
-
虚析构函数 主要是为了解决 "通过基类指针删除派生类对象" (即场景 C)这一特定情况下的内存泄漏问题。除此之外的其他正常对象创建和销毁,C++ 的默认机制都能保证基类析构函数被自动调用。
Q5: 虚函数表存在哪里?虚表指针存在哪里?
-
答:
-
虚表指针 (vptr) :存在于对象的内存空间头部(通常)。
-
虚函数表 (vftable) :存在于代码段(常量区) 。同类型的对象共享同一张虚表。
-
7. 总结
多态是 C++ 面向对象编程的灵魂。如果说封装是让代码"模块化",继承是让代码"复用",那么多态则是让代码变得"灵活"。
通过本文的学习,我们不仅掌握了 virtual、override 等语法细节,更理解了多态背后的设计哲学:"接口与实现分离" 。它允许我们在不修改现有代码的前提下,通过增加新的子类来扩展功能(符合开闭原则)。虽然虚函数表机制在底层带来了一定的性能开销(空间上的 vptr 和时间上的查表跳转),但换来的是极高的代码可维护性和扩展性。
希望大家在理解底层原理的基础上,能够在未来的项目设计中灵活运用多态,写出真正"高内聚、低耦合"的优雅代码。
以上就是本期博客的全部内容,感谢各位的阅读以及观看。如果内容有误请大佬们多多指教,一定积极改进,加以学习。