面试题答案
一键面试C++ 虚函数表支持运行时多态的原理
- 多态的概念基础:在 C++ 中,运行时多态允许通过基类指针或引用调用派生类中重写的虚函数。这依赖于动态绑定机制,即程序在运行时根据对象的实际类型来决定调用哪个函数版本。
- 虚函数表(VTable):每个包含虚函数的类都有一个虚函数表。虚函数表是一个函数指针数组,其中每个元素指向该类中的一个虚函数。当一个对象被创建时,如果它所属的类包含虚函数,那么该对象的内存布局中会包含一个指向虚函数表的指针(通常称为虚函数表指针,vptr)。
- 运行时多态的实现:当通过基类指针或引用调用虚函数时,程序首先通过对象的虚函数表指针找到对应的虚函数表,然后根据虚函数在表中的索引找到要调用的实际函数。这样,即使指针或引用的静态类型是基类,但在运行时可以根据对象的动态类型(实际类型)来调用正确的虚函数版本,从而实现运行时多态。
虚函数被重写后虚函数表指针和虚函数表的变化过程
- 类继承体系下的虚函数表:
- 基类虚函数表:基类的虚函数表包含基类中定义的虚函数的指针。例如,假设有一个基类
Base
有两个虚函数virtual void func1()
和virtual void func2()
,其虚函数表会有两个元素,分别指向这两个虚函数的实现。 - 派生类虚函数表:当一个派生类继承自包含虚函数的基类时,派生类会有自己的虚函数表。如果派生类没有重写基类的虚函数,那么派生类虚函数表中的指针将直接指向基类虚函数的实现。
- 虚函数重写时的变化:当派生类重写了基类的某个虚函数时,派生类虚函数表中对应虚函数的指针会被更新为指向派生类中重写的函数实现。例如,派生类
Derived
重写了Base
中的func1()
,那么Derived
虚函数表中func1
对应的指针将指向Derived::func1()
的实现,而func2
若未重写,其指针仍指向Base::func2()
的实现。
- 基类虚函数表:基类的虚函数表包含基类中定义的虚函数的指针。例如,假设有一个基类
- 虚函数表指针(vptr)的变化:对象的虚函数表指针在对象构造时被初始化。对于派生类对象,在其构造过程中,首先调用基类的构造函数,此时对象的虚函数表指针会被初始化为指向基类的虚函数表。然后,在派生类自身的构造函数执行过程中,如果有虚函数被重写,虚函数表指针会被调整为指向派生类的虚函数表(其中已更新了重写虚函数的指针)。在析构时,顺序相反,先调用派生类析构函数,此时虚函数表指针若指向派生类虚函数表,之后调用基类析构函数,虚函数表指针会变回指向基类虚函数表(如果对象还未完全析构)。
结合汇编代码说明
以下以简单的 C++ 代码示例结合汇编代码(以 x86 - 64 架构下的 GCC 编译器为例)来进一步说明。
class Base {
public:
virtual void func1() {
// 函数实现
}
virtual void func2() {
// 函数实现
}
};
class Derived : public Base {
public:
void func1() override {
// 重写的函数实现
}
};
- 对象创建时虚函数表指针的初始化:
- 当创建
Derived
对象时,汇编代码中在调用Base
构造函数期间会初始化虚函数表指针指向Base
的虚函数表。例如,在Base
构造函数中可能有类似以下的汇编代码(简化示意):
mov rax, [rip + Base_vtable] ; 加载 Base 虚函数表地址到 rax mov qword ptr [rdi], rax ; 将虚函数表地址存入对象的开头(rdi 指向对象)
- 之后在
Derived
构造函数中,如果有虚函数重写,会更新虚函数表指针指向Derived
的虚函数表。
- 当创建
- 虚函数调用时:
- 假设通过
Base* ptr = new Derived(); ptr->func1();
调用虚函数。汇编代码在调用func1
时,首先会从对象ptr
指向的内存开头获取虚函数表指针(因为虚函数表指针在对象开头),然后根据func1
在虚函数表中的偏移找到对应的函数地址并调用。例如:
这里如果mov rax, [rdi] ; 从对象 ptr 指向的地址获取虚函数表指针到 rax mov rax, [rax + 8] ; 假设 func1 在虚函数表中的偏移为 8 字节,获取 func1 的函数地址到 rax call rax ; 调用函数
ptr
实际指向Derived
对象,由于Derived
虚函数表中func1
已更新为指向Derived::func1()
,所以会调用到派生类重写的版本,实现了运行时多态。 - 假设通过