基于本文回答

播面 播面

文图音视,全方位拆解八股文
0
评论

C++ 是如何实现多态的?

知识点图片

在 C++ 中,多态(Polymorphism)是指“同一接口,多种实现”。C++ 实现多态主要分为两大类:静态多态(编译时多态)动态多态(运行时多态)

面试和底层原理中讨论得最多的通常是动态多态(通过虚函数表实现)。下面为你全面解析 C++ 是如何实现这两种多态的。


一、 静态多态(编译时多态)

静态多态是在编译阶段就已经决定了要调用哪个函数,也称为早绑定(Early Binding)或静态绑定。它的实现机制主要有两种:

  1. 函数重载(Function Overloading)
    • 原理:在同一个作用域内,允许存在多个同名函数,只要它们的参数列表(参数个数、类型、顺序)不同即可。
    • 底层实现:编译器在编译时,会根据参数列表对函数名进行名字修饰(Name Mangling)。例如 void print(int)void print(double) 在编译后会被重命名为类似 _Z5printi_Z5printd 的完全不同的函数。
  2. 模板(Templates)
    • 原理:允许编写泛型代码。
    • 底层实现:编译器在编译时,会根据传入的具体类型进行模板实例化,为每一种类型生成一份专属的代码。

二、 动态多态(运行时多态)—— 核心重点

动态多态是在程序运行阶段才决定调用哪个函数,称为迟绑定(Late Binding)或动态绑定。

1. 动态多态成立的三个核心条件

  1. 继承关系:必须存在基类和派生类。
  2. 虚函数重写:基类中必须有 virtual 声明的虚函数,派生类必须重写(override)该虚函数。
  3. 基类指针/引用指向派生类对象:通过基类的指针或引用来调用虚函数。

2. 底层实现机制:虚函数表(V-Table)与虚表指针(vptr)

C++ 的动态多态是依赖虚函数表(Virtual Table,简称 vtable)虚表指针(Virtual Pointer,简称 vptr)来实现的。

  • 虚函数表(vtable)
    • 归属:属于级别,每个含有虚函数的类在编译时都会生成一个唯一的虚函数表。
    • 内容:本质上是一个函数指针数组,里面记录了该类所有的虚函数地址。如果派生类重写了基类的虚函数,派生类虚函数表中的对应位置就会被替换为派生类的虚函数地址。
    • 存储位置:通常存储在可执行文件的只读数据段(.rodata)。
  • 虚表指针(vptr)
    • 归属:属于对象级别。当类实例化为对象时,如果该类有虚函数,编译器会在对象的内存布局的起始位置(或末尾,取决于编译器,一般是起始)偷偷插入一个指针,这就是 vptr
    • 内容:指向该对象所属类的虚函数表(vtable)。
    • 初始化:在对象的构造函数执行时,编译器会自动插入代码来初始化 vptr,使其指向对应的 vtable

3. 动态调用的全过程(图解与原理)

假设有如下代码:

cpp
class Animal {
public:
    virtual void speak() { cout << "Animal speaks" << endl; }
};

class Dog : public Animal {
public:
    void speak() override { cout << "Dog barks" << endl; } // 重写虚函数
};

int main() {
    Animal* p = new Dog(); // 基类指针指向派生类对象
    p->speak();            // 触发动态多态
    return 0;
}

运行 p->speak() 时,底层发生了什么?

  1. 编译器发现 speak() 是一个虚函数,且 p 是一个指针。因此它不会直接生成调用 Animal::speak() 的汇编代码,而是生成一段间接寻址的代码。
  2. 程序运行时,通过指针 p 找到它所指向的 Dog 对象。
  3. 取出 Dog 对象内存中的隐藏成员:虚表指针(vptr)
  4. 通过 vptr 找到 Dog 类的虚函数表(vtable)
  5. vtable 中查找 speak() 函数对应的偏移量(在此例中可能是表中的第一个元素)。
  6. 发现表中存放的是 Dog::speak() 的地址,于是跳转到该地址执行。

转化为类似 C 语言的伪代码,多态调用的本质如下:

c
// p->speak(); 在底层的实际逻辑类似于:
(*p->vptr[speak_offset])(p); 
// p是作为隐藏的 this 指针传入的

三、 多态相关的面试高频考点

1. 虚析构函数(Virtual Destructor)

  • 问题:为什么基类的析构函数通常要声明为 virtual
  • 答案:如果基类析构函数不是虚函数,当我们用 delete p; 删除一个指向派生类对象的基类指针时,由于是静态绑定,编译器只会调用基类的析构函数,而不会调用派生类的析构函数,这就导致派生类中独有的资源(如动态分配的内存)无法释放,造成内存泄漏。声明为虚函数后,析构行为也会走虚表,从而先调用派生类析构,再调用基类析构。

2. 构造函数可以是虚函数吗?

  • 答案不能
  • 原因:虚函数的调用依赖对象的 vptr。而 vptr 是在对象的构造函数内部才被初始化的。如果构造函数是虚函数,调用它时 vptr 还没建好,自然无法去查虚函数表。此外,构造对象时必须确切知道对象的实际类型,不需要多态机制。

3. 纯虚函数与抽象类

  • 如果在基类中定义虚函数时加上 = 0(如 virtual void speak() = 0;),它就变成了纯虚函数
  • 包含至少一个纯虚函数的类叫做抽象类,抽象类不能被实例化(不能创建对象)。只能被继承,且派生类必须实现所有的纯虚函数才能被实例化。

4. 多态的代价(性能开销)

虽然多态极大地提高了代码的可扩展性,但也有成本:

  • 空间开销:每个带有虚函数的类都需要一个虚函数表(vtable);每个对象都会多出一个指针(vptr)的内存空间(32位系统占4字节,64位系统占8字节)。
  • 时间开销:调用虚函数需要额外的指针寻址(先找 vptr,再查 vtable,再跳转执行),比普通的函数直接跳转要慢一点。同时,虚函数通常无法被编译器内联(inline)优化

总结

C++ 的多态机制是一套极其精巧的设计。静态多态靠编译器在编译时进行函数重载决议和模板推导;动态多态则是通过继承配合虚函数表 (vtable) 和虚指针 (vptr),在程序运行时进行查表和动态跳转来实现的。

00:00
00:00