【C++】C++ 中多态是什么?咋用的?
在 C++ 中,“多态”(Polymorphism)是个你早晚会碰到的概念。
它是“同一接口,不同实现”这一思想的落地方案。
而且——它不仅仅是语法糖,背后还有一套完整的运行机制和性能权衡。
今天我们讲多态~
1. 多态的基本概念
在 C++ 中,多态主要分为两类:
-
编译时多态(静态多态)
- 发生在编译阶段,比如函数重载(overloading)和模板(templates)。
- 编译器在编译期间就能决定调用哪一个函数版本,没有运行时开销。
所以之前我们讲到的模板和重载等知识,其实就是多态下的一种类型~
-
运行时多态(动态多态)
- 通过继承 + 虚函数(virtual function)实现。
- 具体调用哪个函数,要等到程序运行的时候才能确定(依赖虚函数表)。
继承和虚函数的概念前面也讲过了,因为有一个具体查表的过程,所以被列为动态的多态。
今天我们主要聊第二种——因为这才是大多数人提到“多态”时的意思。
2. 运行时多态是怎么工作的?
先看一个例子:
#include <iostream>
using namespace std;class Instrument {
public:virtual void play() {cout << "Playing some instrument" << endl;}
};class Guitar : public Instrument {
public:void play() override {cout << "Playing guitar" << endl;}
};class Piano : public Instrument {
public:void play() override {cout << "Playing piano" << endl;}
};int main() {Instrument* i1 = new Guitar();Instrument* i2 = new Piano();i1->play(); // 输出:Playing guitari2->play(); // 输出:Playing pianodelete i1;delete i2;
}
我们看这个例子,
virtual
让play()
成为虚函数。 当你用基类指针指向派生类对象时,调用play()
会根据对象的实际类型来决定执行哪个版本。- 底层原理是什么呢?底层原理是虚函数表(vtable) :每个含有虚函数的类都会有一张函数指针表,指向实际要执行的函数。接下来具体讲解下这个表是怎么个事。
3.虚函数表
想象一下,有这样一个类:
class Base {
public:virtual void func1();virtual void func2();
};
当编译器发现类中有虚函数时,它会:
- 为类生成一张虚函数表(vtable):本质上是一个函数指针数组,存放着当前类的虚函数入口地址。
- 在每个对象中,悄悄插入一个指针(vptr) ,指向该类的虚函数表。
如果有派生类覆盖了虚函数,比如:
class Derived : public Base {
public:void func1() override; // 覆盖 func1
};
那么:
Derived
的 vtable 里,func1
会指向新实现的版本;- 但
func2
会继续沿用Base
的实现。
调用过程:
Base* obj = new Derived();
obj->func1();
执行时:
- 通过
obj
找到对象的vptr
; - 根据
vptr
定位到Derived
的 vtable; - 找到
func1
在表中的位置,调用对应的函数地址。
这样,就实现了“同一个函数调用,在不同对象上执行不同的代码”——这就是运行时多态。
所以怎么说呢,虚函数表就相当于是给定了指针,那么就直接通过地址来找你的具体实现方法,这样就可以避免函数冗余等问题,这也是多态的核心表。
那为啥要用这种机制呢?
4. 为什么要用这种机制?
C++ 是强类型、静态编译的语言,如果没有虚函数表机制,所有函数调用的目标地址在编译时就已经确定,根本没法在运行时根据对象类型切换行为。
虚函数表的意义就是:
- 让编译器能在运行时,根据对象的实际类型,动态选择函数实现;
- 同时保持语法上的统一(基类指针/引用就能调用不同对象的实现)。
5. 戳细节!
一旦理解了 vtable,就能顺理成章地理解多态相关的几个常见知识点:
5.1 基类析构函数必须是虚函数
如果析构函数不是 virtual
,delete
基类指针时只会调用基类析构,而不会调用派生类析构,导致资源泄漏。
因为析构过程也是通过 vtable 调用的,缺少虚析构会让派生类的析构函数没有入口。
5.2 虚函数有运行时开销
每次调用虚函数都需要有以下步骤:
- 访问对象的
vptr
(一次内存读取) - 访问 vtable(一次内存读取)
- 再跳转到目标函数(一次间接跳转)
这会让虚函数的调用性能比普通函数略差,且不能内联(inline)。有点繁琐是难免的()
5.3 多继承与菱形继承
- 多继承:如果一个类有多个虚函数表(因为继承多个含虚函数的基类),对象会有多个
vptr
。 - 虚拟继承:编译器会调整 vtable 的布局,确保共享的基类只有一份实例。
这些机制都会增加对象布局的复杂度和内存占用。也就是我们上面说的会有函数冗余,菱形继承也是冗余的一种。
5.4 纯虚函数与接口抽象
class Shape {
public:virtual void draw() = 0; // 纯虚函数
};
纯虚函数的类不能直接实例化,只能作为接口存在。
这是一种典型的多态应用模式:面向接口编程。
6. 总结~
多态的本质(这里主要是运行时多态):
基于虚函数表和 vptr,通过运行时查表来动态决定调用哪个函数版本。
理解多态是理解C++中函数复用以及不同场景不同方法的基础,这其实也是我们现实生活中很多物品的分类方法~由大到小嘛