【C++】C++ 中多态是什么?咋用的?

71 阅读5分钟

在 C++ 中,“多态”(Polymorphism)是个你早晚会碰到的概念。
它是“同一接口,不同实现”这一思想的落地方案。
而且——它不仅仅是语法糖,背后还有一套完整的运行机制和性能权衡。

今天我们讲多态~


1. 多态的基本概念

在 C++ 中,多态主要分为两类:

  1. 编译时多态(静态多态)

    • 发生在编译阶段,比如函数重载(overloading)和模板(templates)。
    • 编译器在编译期间就能决定调用哪一个函数版本,没有运行时开销。

    所以之前我们讲到的模板和重载等知识,其实就是多态下的一种类型~

  2. 运行时多态(动态多态)

    • 通过继承 + 虚函数(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 guitar
    i2->play();  // 输出:Playing piano

    delete i1;
    delete i2;
}

我们看这个例子,

  • virtualplay() 成为虚函数。 当你用基类指针指向派生类对象时,调用 play() 会根据对象的实际类型来决定执行哪个版本。
  • 底层原理是什么呢?底层原理是虚函数表(vtable) :每个含有虚函数的类都会有一张函数指针表,指向实际要执行的函数。接下来具体讲解下这个表是怎么个事。

3.虚函数表

想象一下,有这样一个类:

class Base {
public:
    virtual void func1();
    virtual void func2();
};

当编译器发现类中有虚函数时,它会:

  1. 为类生成一张虚函数表(vtable):本质上是一个函数指针数组,存放着当前类的虚函数入口地址。
  2. 在每个对象中,悄悄插入一个指针(vptr) ,指向该类的虚函数表。

如果有派生类覆盖了虚函数,比如:

class Derived : public Base {
public:
    void func1() override; // 覆盖 func1
};

那么:

  • Derived 的 vtable 里,func1 会指向新实现的版本;
  • func2 会继续沿用 Base 的实现。

调用过程:

Base* obj = new Derived();
obj->func1();

执行时:

  1. 通过 obj 找到对象的 vptr
  2. 根据 vptr 定位到 Derived 的 vtable;
  3. 找到 func1 在表中的位置,调用对应的函数地址。

这样,就实现了“同一个函数调用,在不同对象上执行不同的代码”——这就是运行时多态。

所以怎么说呢,虚函数表就相当于是给定了指针,那么就直接通过地址来找你的具体实现方法,这样就可以避免函数冗余等问题,这也是多态的核心表。

那为啥要用这种机制呢?


4. 为什么要用这种机制?

C++ 是强类型、静态编译的语言,如果没有虚函数表机制,所有函数调用的目标地址在编译时就已经确定,根本没法在运行时根据对象类型切换行为。

虚函数表的意义就是:

  • 让编译器能在运行时,根据对象的实际类型,动态选择函数实现;
  • 同时保持语法上的统一(基类指针/引用就能调用不同对象的实现)。

5. 戳细节!

一旦理解了 vtable,就能顺理成章地理解多态相关的几个常见知识点:

5.1 基类析构函数必须是虚函数

如果析构函数不是 virtualdelete 基类指针时只会调用基类析构,而不会调用派生类析构,导致资源泄漏。

因为析构过程也是通过 vtable 调用的,缺少虚析构会让派生类的析构函数没有入口。


5.2 虚函数有运行时开销

每次调用虚函数都需要有以下步骤:

  1. 访问对象的 vptr(一次内存读取)
  2. 访问 vtable(一次内存读取)
  3. 再跳转到目标函数(一次间接跳转)

这会让虚函数的调用性能比普通函数略差,且不能内联(inline)。有点繁琐是难免的()


5.3 多继承与菱形继承

  • 多继承:如果一个类有多个虚函数表(因为继承多个含虚函数的基类),对象会有多个 vptr
  • 虚拟继承:编译器会调整 vtable 的布局,确保共享的基类只有一份实例。

这些机制都会增加对象布局的复杂度和内存占用。也就是我们上面说的会有函数冗余,菱形继承也是冗余的一种。


5.4 纯虚函数与接口抽象

class Shape {
public:
    virtual void draw() = 0; // 纯虚函数
};

纯虚函数的类不能直接实例化,只能作为接口存在。
这是一种典型的多态应用模式:面向接口编程


6. 总结~

多态的本质(这里主要是运行时多态):

基于虚函数表和 vptr,通过运行时查表来动态决定调用哪个函数版本。

理解多态是理解C++中函数复用以及不同场景不同方法的基础,这其实也是我们现实生活中很多物品的分类方法~由大到小嘛