C++ 虚函数表原理与内存结构深度解析

Dylan
Dylan
发布时间:2026-10-13 | 6人浏览过
原创

C++的动态多态依赖于虚函数表(Virtual Table)。对于有虚函数的类,其对象在内存头部包含一个指向虚函数表的指针。本文将通过代码和内存分析,揭示虚函数表的内部排列、单继承下的函数重写规则,以及多重继承时多个虚函数表的组织方式。

准备环境与虚函数表基础

要观察 C++ 对象的内存布局,首先需要明确一点:当一个类包含虚函数时,编译器会在该类每个实例对象的内存起始位置插入一个隐藏的指针,即 vptr(虚函数表指针),它指向类的虚函数表。vtbl(虚函数表)是一个函数指针数组,按声明顺序存放了该类所有虚函数的地址。

为了验证这一机制,我们定义一个包含三个虚函数的基类 Base:

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

在 64 位系统上,指针占用 8 个字节。因此 sizeof(Base) 的结果应为 8(仅包含 vptr)。我们可以通过强转内存地址来直接访问这个表和其中的函数指针。

  1. 获取虚函数表地址:将对象地址 &b 强转为长整型指针并解引用,即可得到 vtbl 的地址。
  2. 获取具体函数地址:vtbl 的第一项是 f1 的地址,第二项是 f2 的地址(偏移 8 字节),依此类推。
  3. 调用函数:将取得的函数地址强转为函数指针类型并进行调用。
int main() {
    typedef void(*Fun)();
    Base b;

    // 1. 获取虚函数表地址
    long v_table_addr_value = *(long*)&b;
    void *v_table_addr = (void*)v_table_addr_value;

    // 2. 获取并调用 f1()
    long f1_addr_value = *(long*)v_table_addr;
    Fun f1 = (Fun)f1_addr_value;
    f1(); // 输出: Base::f1

    // 3. 获取并调用 f2() (偏移 8 字节)
    long f2_addr_value = *(long*)((char*)v_table_addr + 8);
    Fun f2 = (Fun)f2_addr_value;
    f2(); // 输出: Base::f2

    return 0;
}

执行上述代码可以验证内存模型如下:

从图中可以看到,对象起始处存储的是 vtbl 的指针,随后依次是 f1、f2、f3 的函数地址。

单继承下的虚函数表变化

当子类 Derived 继承自 Base 并重写(Override)了部分虚函数时,虚函数表会发生特定的调整。

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

在此情况下,Derived 的内存模型遵循以下规则:

  1. 父类顺序保留:基类的虚函数依然排在前面。
  2. 覆盖替换:被重写的 f1 会替换掉 vtbl 中原来的 Base::f1 位置。
  3. 新增追加:新增加的虚函数 f4 会被追加到列表末尾。
  4. 未被覆盖不变:f2 和 f3 的位置和地址保持不变。

验证后的内存结构如下图所示:

这种机制保证了当使用基类指针或引用指向子类对象时,能够正确地调用子类的版本,实现了多态的核心需求。

多重继承的复杂情况

多重继承引入了更复杂的内存布局。当一个类继承自多个含有虚函数的基类时,它将拥有多个虚函数表,分别对应每个父类的接口。

class Base1 { virtual void f1(); virtual void f2(); };
class Base2 { virtual void f1(); virtual void f2(); };
class Base3 { virtual void f1(); virtual void f2(); };

class Derived : public Base1, public Base2, public Base3 {
public:
    void f1() override {} // 重写所有父类的f1
    virtual void f4() {} // 新增虚函数
};

由于派生类需要满足所有基类的多态需求,编译器会为 Derived 对象分配额外的空间来存储其他基类的虚表指针。通常对象起始位置存储的是第一个基类(最左继承)的 vptr。

内存分布如下:

  1. 第一段(Base1):包含 f1(已覆盖为 Derived::f1)、f2(原 Base1::f2)以及新增的 f4 等。
  2. 第二段(Base2):第二个 vptr,包含 f1(已覆盖为 Derived::f1)和 f2(原 Base2::f2)。
  3. 第三段(Base3):第三个 vptr,包含 f1(已覆盖为 Derived::f1)和 f2(原 Base3::f2)。

通过代码偏移寻址可以观察到这 3 个独立的虚表指针:

  • 第 1 个表指针在偏移 0 处。
  • 第 2 个表指针在偏移 8 处。
  • 第 3 个表指针在偏移 16 处。

验证结果示意图:

由此可见:
- 有几个带虚函数的基类,就产生几个虚表指针。
- 派生类新增的虚函数只添加到第一个(主)虚表中,位于基类成员之后。
- 对各个父类同名虚函数的重写,会同时替换各个父类对应虚表中的函数指针。

调用被覆盖的原始虚函数

在实际开发中,有时需要在子类中显式调用被自己覆盖的父类虚函数。这可以通过作用域解析运算符 :: 来实现强制指定。

Derived d;
d.f1();              // 调用 Derived::f1
d.Base::f1();        // 显式调用 Base::f1

总结来说,理解虚函数表不仅有助于调试多态行为,更能让我们明白 C++ 对象模型的底层代价。多重继承虽然灵活,但带来的多份虚表指针会增加对象体积和访问复杂度,因此在设计时应谨慎权衡。

以上就是C++虚函数表原理与内存结构详细解析的内容,更多关于C++面向对象高级特性的资料请关注本站其它相关文章!

相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表