MST

星途 面试题库

面试题:C++ 内存模型中的虚函数表与内存布局

请详细描述C++中虚函数表的工作原理以及它在内存中的布局方式。当一个类继承自多个含有虚函数的基类时,内存布局会发生怎样的变化?如何通过指针或引用访问虚函数表中的函数?
38.6万 热度难度
编程语言C++

知识考点

AI 面试

面试题答案

一键面试

虚函数表工作原理

  1. 动态绑定基础:在C++中,虚函数表(vtable)是实现多态性的关键机制。当一个类包含虚函数时,编译器会为该类创建一个虚函数表。虚函数表本质上是一个函数指针数组,每个元素指向该类的一个虚函数的实际实现。
  2. 对象的vptr:每个包含虚函数的类的对象,都隐含地包含一个指向虚函数表的指针(vptr)。当通过指针或引用调用虚函数时,实际调用的函数是根据对象的动态类型,通过vptr找到虚函数表,然后从虚函数表中获取对应函数指针并调用。例如:
class Base {
public:
    virtual void func() { std::cout << "Base::func" << std::endl; }
};
class Derived : public Base {
public:
    void func() override { std::cout << "Derived::func" << std::endl; }
};
Base* ptr = new Derived();
ptr->func(); 

这里ptr虽然是Base*类型,但实际指向Derived对象,调用func时会通过Derived对象的vptr找到Derived的虚函数表,从而调用Derived::func

内存布局方式

  1. 单继承:在单继承且基类有虚函数的情况下,对象内存布局中,vptr通常位于对象内存布局的起始位置,紧接着是对象的数据成员。例如:
class Base {
public:
    virtual void func() {}
    int data;
};

Base对象内存布局为:vptr(指向Base的虚函数表),然后是data。 2. 多继承且基类含虚函数:当一个类继承自多个含有虚函数的基类时,内存布局会变得复杂。每个基类都可能有自己的虚函数表。派生类对象的内存布局中,可能会有多个vptr,分别对应不同基类的虚函数表。例如:

class Base1 {
public:
    virtual void func1() {}
};
class Base2 {
public:
    virtual void func2() {}
};
class Derived : public Base1, public Base2 {
public:
    void func1() override {}
    void func2() override {}
};

Derived对象内存布局可能是:Base1的vptr(指向Base1虚函数表,其中func1可能被Derived::func1覆盖),Base1的数据成员(如果有),Base2的vptr(指向Base2虚函数表,其中func2可能被Derived::func2覆盖),Base2的数据成员(如果有),Derived自己的数据成员。

通过指针或引用访问虚函数表中的函数

  1. 通过对象指针:当有一个指向包含虚函数的类对象的指针时,编译器在编译时无法确定实际调用的虚函数版本。运行时,会根据指针所指对象的实际类型,通过对象的vptr找到对应的虚函数表,再从虚函数表中获取函数指针并调用。如上述Base* ptr = new Derived(); ptr->func();的例子。
  2. 通过对象引用:通过对象引用调用虚函数原理与指针类似。引用在底层也是通过指针实现的,同样会根据引用所绑定对象的实际类型,利用vptr找到虚函数表来调用相应虚函数。例如:
Base& ref = *new Derived();
ref.func(); 

这里ref绑定Derived对象,调用func时会通过Derived对象的vptr找到正确的虚函数。

注意,直接通过虚函数表指针(vptr)来访问虚函数表中的函数是不推荐且不安全的,因为虚函数表的布局是编译器相关的。但在一些极端的底层调试或研究场景下,可以通过类型转换和指针运算来间接访问,但这种做法非常依赖编译器实现细节。