虚函数表(vtable)的初始化时机
虚函数表的初始化分为两个阶段,一个是编译/链接期,一个是运行期,二者分工不同:
编译/链接期:vtable 结构的确定(静态阶段)
编写包含虚函数的类(比如 class a { virtual void f(); })并编译时:
- 编译器会为每个包含虚函数的类(或继承了虚函数的子类)生成一份固定的 vtable 数据结构(本质是函数指针数组)。
- 这个 vtable 会被编译到程序的只读数据段(.rodata)中,和代码段一样,是程序启动前就确定好的“静态数据”。
- 此时 vtable 里已经填好了各个虚函数的地址(比如
a::f的地址)。
运行期:vptr(虚函数表指针)的初始化(动态阶段)
每个包含虚函数的对象,都会有一个隐藏的 vptr 成员(指向所属类的 vtable),这个指针的初始化:
- 对于普通对象:
vptr在对象构造函数执行的最开始阶段完成初始化(早于构造函数的函数体执行)- 子类对象构造时,会先初始化父类的
vptr,再初始化子类自己的vptr。
- 子类对象构造时,会先初始化父类的
- 对于全局/静态对象:
vptr在程序启动后的全局初始化阶段(main 函数执行前)完成。 - 对于 new 创建的对象:
vptr在operator new分配内存后、构造函数执行前初始化。
验证代码(c++)
#include <iostream>
using namespace std;
class base {
public:
base() {
// 此时 vptr 已经指向 base 的 vtable,调用虚函数会走 base 版本
func(); // 输出 "base::func"
}
virtual void func() { cout << "base::func" << endl; }
};
class derived : public base {
public:
derived() {
// 此时 vptr 已经切换到 derived 的 vtable
func(); // 输出 "derived::func"
}
void func() override { cout << "derived::func" << endl; }
};
int main() {
derived d;
// 构造过程:
// 1. 初始化 base 部分 → vptr 指向 base::vtable → 调用 base::func
// 2. 初始化 derived 部分 → vptr 切换到 derived::vtable → 调用 derived::func
return 0;
}
输出结果:
base::func
derived::func
即vptr 在构造函数执行时已经完成初始化,且子类构造时会覆盖父类的 vptr。
常见误区
- ❌ 错误:“每次创建对象都会生成新的 vtable”
✅ 正确:一个类只有一份 vtable(编译期确定),所有对象的vptr都指向这份 vtable。 - ❌ 错误:“vptr 在构造函数体执行后初始化”
✅ 正确:vptr初始化在构造函数的“初始化列表阶段”(编译器自动插入),早于函数体执行。 - ❌ 错误:“虚函数表在运行时动态生成”
✅ 正确:vtable 本身是编译期生成的静态数据,运行时只初始化vptr指向它。
总结
- 虚函数表(vtable)本身:编译/链接期生成,存储在只读数据段,属于类的静态资源,全局唯一。
- 虚函数表指针(vptr):运行期初始化,普通对象在构造函数执行前完成,子类构造时会覆盖父类的
vptr。 - 核心逻辑:vtable 是“静态模板”,vptr 是“动态指针”,二者结合实现多态。
到此这篇关于c++中虚函数表的初始化时机的文章就介绍到这了,更多相关c++虚函数表初始化内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论