MST

星途 面试题库

面试题:C++ 指针在多态与虚函数表中的底层应用

在 C++ 的多态机制中,指针与虚函数表是如何协同工作的?当通过基类指针调用虚函数时,底层的实现原理是什么?请深入分析并结合具体代码说明,包括如何通过指针操作来绕过正常的虚函数调用机制(不推荐实际使用,仅作原理探讨)。
44.0万 热度难度
编程语言C++

知识考点

AI 面试

面试题答案

一键面试

1. 指针与虚函数表的协同工作原理

在 C++ 中,当一个类包含虚函数时,编译器会为该类生成一个虚函数表(vtable)。虚函数表是一个存储虚函数地址的数组。每个包含虚函数的类对象都有一个隐藏的指针,称为虚指针(vptr),该指针指向类的虚函数表。

当通过基类指针调用虚函数时,首先会根据指针所指向对象的实际类型,找到对应的虚函数表。然后,在虚函数表中查找与被调用虚函数对应的函数地址,并调用该函数。这种机制实现了动态绑定,即根据对象的实际类型来决定调用哪个虚函数。

2. 底层实现原理

  1. 虚函数表的生成:编译器为每个包含虚函数的类生成一个虚函数表。虚函数表中的条目按照虚函数在类定义中的声明顺序排列。
  2. 虚指针的初始化:在对象构造时,编译器会将对象的虚指针(vptr)初始化为指向该对象所属类的虚函数表。
  3. 虚函数调用:当通过基类指针调用虚函数时,编译器会根据指针所指向对象的虚指针找到对应的虚函数表,然后在虚函数表中查找并调用相应的虚函数。

3. 具体代码说明

#include <iostream>

class Base {
public:
    virtual void virtualFunction() {
        std::cout << "Base::virtualFunction" << std::endl;
    }
};

class Derived : public Base {
public:
    void virtualFunction() override {
        std::cout << "Derived::virtualFunction" << std::endl;
    }
};

int main() {
    Base* basePtr = new Derived();
    basePtr->virtualFunction(); // 动态绑定,调用 Derived::virtualFunction

    delete basePtr;
    return 0;
}

在上述代码中:

  1. Base 类包含一个虚函数 virtualFunction。编译器会为 Base 类生成一个虚函数表,并在 Base 对象构造时初始化虚指针(vptr)指向该虚函数表。
  2. Derived 类继承自 Base 类并重写了 virtualFunction。编译器会为 Derived 类生成一个新的虚函数表,该表中 virtualFunction 的条目指向 Derived::virtualFunctionDerived 对象构造时,虚指针(vptr)指向 Derived 类的虚函数表。
  3. basePtr->virtualFunction() 被调用时,根据 basePtr 所指向的 Derived 对象的虚指针,找到 Derived 类的虚函数表,进而调用 Derived::virtualFunction

4. 绕过正常虚函数调用机制

虽然不推荐实际使用,但为了探讨原理,可以通过直接操作虚函数表指针来绕过正常的虚函数调用机制。以下是一个示例代码:

#include <iostream>
#include <cstdint>

class Base {
public:
    virtual void virtualFunction() {
        std::cout << "Base::virtualFunction" << std::endl;
    }
};

class Derived : public Base {
public:
    void virtualFunction() override {
        std::cout << "Derived::virtualFunction" << std::endl;
    }
};

typedef void (*VirtualFunctionPtr)();

void customFunction() {
    std::cout << "Custom function" << std::endl;
}

int main() {
    Base* basePtr = new Derived();

    // 获取虚函数表指针
    uintptr_t* vtablePtr = reinterpret_cast<uintptr_t*>(*reinterpret_cast<uintptr_t*>(basePtr));

    // 获取虚函数表中第一个虚函数的地址
    VirtualFunctionPtr originalFunction = reinterpret_cast<VirtualFunctionPtr>(vtablePtr[0]);

    // 修改虚函数表中第一个虚函数的地址为 customFunction 的地址
    vtablePtr[0] = reinterpret_cast<uintptr_t>(customFunction);

    // 调用虚函数,此时会调用 customFunction
    basePtr->virtualFunction();

    // 恢复虚函数表中第一个虚函数的原始地址
    vtablePtr[0] = reinterpret_cast<uintptr_t>(originalFunction);

    // 再次调用虚函数,此时会调用 Derived::virtualFunction
    basePtr->virtualFunction();

    delete basePtr;
    return 0;
}

在上述代码中:

  1. 通过 reinterpret_cast 操作符获取对象的虚函数表指针,并获取虚函数表中第一个虚函数的地址。
  2. 修改虚函数表中第一个虚函数的地址为 customFunction 的地址,从而绕过正常的虚函数调用机制。
  3. 调用虚函数时,会调用 customFunction。最后恢复虚函数表中虚函数的原始地址,使虚函数调用恢复正常。这种做法依赖于特定的编译器和平台实现,可能在不同环境下无法正常工作,并且破坏了 C++ 的封装性和可维护性,因此不推荐在实际开发中使用。