题目

编写程序,揭示 C++ 虚函数机制背后的底层实现机制:虚函数表(vtable)与虚表指针(vptr)。通过 sizeof 比对无虚函数类(0 虚表开销)与含虚函数类(首部植入 8 字节 vptr 指针)的对象内存大小;并使用底层指针强转操作(uintptr_t**)提取出对象首地址处存储的 vptr 虚表地址,进一步索引出虚表中的函数指针并完成手工直接调用。

输入格式:无需外部输入。

输出格式:输出类对象内存尺寸对比、vptr 虚表地址提取与通过函数指针直接调用的回显。

数据范围与约定:底层 ABI 内存布局、vptr 植入、虚表函数指针数组。

思路与算法

1. vptr 的植入位置
只要类中声明了至少一个 virtual 函数,编译器就会在每个对象的头部(通常是 offset 0 处)插入一个隐式指针成员 void** __vptr(在 64 位系统下占用 8 字节)。

2. vtable 是全局只读数据
同一类的所有实例对象,其内部的 vptr 均指向同一张属于该类的全局只读虚表(vtable)。vtable 本质上是一个存储成员函数入口地址的指针数组。

3. 多态调用的汇编本质
p->foo() 在底层等价于:①从 p 首地址取出 vptr;②通过 vptr[index] 取出函数指针;③跳转执行 call (*funcPtr)(p)。

完整程序

#include <iostream>
#include <cstdint>

class PlainClass {
    int a;
    int b;
};

class PolymorphicClass {
public:
    int a;
    int b;

    virtual void funcA() {
        std::cout << "  [底层执行] PolymorphicClass::funcA() 被成功调用!\n";
    }

    virtual void funcB() {
        std::cout << "  [底层执行] PolymorphicClass::funcB() 被成功调用!\n";
    }

    virtual ~PolymorphicClass() = default;
};

// 定义普通函数指针类型
typedef void (*FuncPtr)(PolymorphicClass*);

int main() {
    std::cout << "--- 1. 对象内存尺寸对比 (64位系统) ---\n";
    std::cout << "无虚函数类 PlainClass 大小: " << sizeof(PlainClass) << " 字节 (2 个 int)\n";
    std::cout << "含虚函数类 PolymorphicClass 大小: " << sizeof(PolymorphicClass) 
              << " 字节 (vptr 8 字节 + 2 个 int 8 字节)\n\n";

    // 2. 深入内存探测 vptr
    PolymorphicClass obj;
    obj.a = 100;
    obj.b = 200;

    std::cout << "--- 2. 提取对象头部的 vptr 虚表指针 ---\n";
    uintptr_t* vptr = *(reinterpret_cast<uintptr_t**>(&obj));
    std::cout << "vptr 是否为空: " << (vptr == nullptr ? "是" : "否") << "\n";

    // 虚表中第 0 个槽位是 funcA,第 1 个槽位是 funcB
    std::cout << "\n--- 3. 绕过正常调用,纯靠虚表指针直接调用虚函数 ---\n";
    FuncPtr fA = reinterpret_cast<FuncPtr>(vptr[0]);
    fA(&obj); // 手工传 this 指针直接触发调用

    return 0;
}

运行示例

输出:

--- 1. 对象内存尺寸对比 (64位系统) ---
无虚函数类 PlainClass 大小: 8 字节 (2 个 int)
含虚函数类 PolymorphicClass 大小: 16 字节 (vptr 8 字节 + 2 个 int 8 字节)

--- 2. 提取对象头部的 vptr 虚表指针 ---
vptr 是否为空: 否

--- 3. 绕过正常调用,纯靠虚表指针直接调用虚函数 ---
  [底层执行] PolymorphicClass::funcA() 被成功调用!