C++ 虚函数表原理与内存结构深度解析
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)。我们可以通过强转内存地址来直接访问这个表和其中的函数指针。
- 获取虚函数表地址:将对象地址
&b强转为长整型指针并解引用,即可得到vtbl的地址。 - 获取具体函数地址:
vtbl的第一项是f1的地址,第二项是f2的地址(偏移 8 字节),依此类推。 - 调用函数:将取得的函数地址强转为函数指针类型并进行调用。
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 的内存模型遵循以下规则:
- 父类顺序保留:基类的虚函数依然排在前面。
- 覆盖替换:被重写的
f1会替换掉vtbl中原来的Base::f1位置。 - 新增追加:新增加的虚函数
f4会被追加到列表末尾。 - 未被覆盖不变:
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。
内存分布如下:
- 第一段(Base1):包含
f1(已覆盖为Derived::f1)、f2(原Base1::f2)以及新增的f4等。 - 第二段(Base2):第二个
vptr,包含f1(已覆盖为Derived::f1)和f2(原Base2::f2)。 - 第三段(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++面向对象高级特性的资料请关注本站其它相关文章!
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













