MST
星途 面试题库

面试题:C++虚函数与普通成员函数在内存布局和调用机制上的差异

详细描述C++中虚函数和普通成员函数在内存布局上是如何体现的,以及它们各自的调用机制是怎样的。结合对象模型和汇编代码分析,解释虚函数表(vtable)在虚函数调用过程中起到的作用。
44.6万 热度难度
编程语言C++

知识考点

AI 面试

面试题答案

一键面试

1. 内存布局体现

  • 普通成员函数:普通成员函数并不占用对象的内存空间。类的对象在内存中主要存储其成员变量。多个对象的普通成员函数代码是共享的,存储在代码段。例如:
class A {
public:
    int data;
    void func() {
        // 函数体
    }
};

在这种情况下,A类对象只包含data成员变量的内存空间,func函数的代码存储在代码段,所有A类对象共用这段代码。

  • 虚函数:当类中包含虚函数时,对象的内存布局会增加一个虚指针(vptr)。虚指针指向虚函数表(vtable)。虚函数表是一个存储虚函数地址的数组。例如:
class B {
public:
    int data;
    virtual void vfunc() {
        // 函数体
    }
};

B类对象在内存中除了data成员变量外,还会有一个虚指针。虚指针指向的虚函数表中存储了vfunc函数的地址。如果B类有多个虚函数,虚函数表会按顺序存储这些虚函数的地址。

2. 调用机制

  • 普通成员函数:普通成员函数的调用是在编译期确定的。编译器根据对象的类型直接生成调用函数的机器指令。例如:
A a;
a.func();

编译器在编译时就知道aA类对象,直接生成调用A::func的指令。这种调用方式效率较高,因为不需要额外的查找操作。

  • 虚函数:虚函数的调用是在运行期确定的。当通过对象指针或引用调用虚函数时,程序首先会根据对象中的虚指针找到虚函数表,然后在虚函数表中查找对应虚函数的地址,最后调用该地址处的函数。例如:
B b;
B* ptr = &b;
ptr->vfunc();

在运行时,程序根据ptr指向的对象中的虚指针找到虚函数表,再从虚函数表中找到vfunc的地址并调用。

3. 虚函数表(vtable)在虚函数调用过程中的作用

以以下代码结合汇编分析:

class Base {
public:
    virtual void vfunc() {
        // 函数体
    }
};

class Derived : public Base {
public:
    void vfunc() override {
        // 函数体
    }
};

int main() {
    Base* basePtr = new Derived();
    basePtr->vfunc();
    delete basePtr;
    return 0;
}

在汇编层面,当执行basePtr->vfunc();时:

  1. 首先从basePtr指向的对象内存起始位置获取虚指针(假设对象内存起始地址为ebp - 8,虚指针通常位于对象内存起始位置)。
  2. 通过虚指针找到虚函数表的地址。
  3. 在虚函数表中,根据虚函数的索引(虚函数在虚函数表中的位置)找到具体虚函数的地址。在这个例子中,Derived类重写了vfunc,所以虚函数表中对应vfunc的地址是Derived::vfunc的地址。
  4. 调用找到的虚函数地址处的函数。

虚函数表的作用就是在运行时为虚函数调用提供一个间接寻址的机制,使得程序能够根据对象的实际类型(而不是指针或引用的静态类型)来调用正确的虚函数,从而实现多态性。