c++虚表的调用
前言
虚表(Virtual Table,简称 vtable)是 C++ 实现运行时多态(动态绑定)的核心机制。下面详细解释虚表的调用过程:
虚表的基本概念
-
虚表是什么:每个包含虚函数的类都有一个虚表,它是一个函数指针数组,存储该类所有虚函数的地址。
-
虚指针:每个包含虚函数的类的对象都有一个隐藏的虚指针(vptr),指向该类的虚表。
虚表调用过程
当通过基类指针或引用调用虚函数时,会发生以下步骤:
-
获取虚指针:通过对象中的 vptr 找到虚表
-
查找函数地址:在虚表中找到对应的虚函数地址
-
调用函数:通过函数指针调用实际的函数
class Animal {
public:virtual void speak() = 0;virtual ~Animal() {}
};class Dog : public Animal {
public:void speak() override { cout << "Woof!" << endl; }
};class Cat : public Animal {
public:void speak() override { cout << "Meow!" << endl; }virtual void purr() { cout << "Purr..." << endl; }
};void makeSound(Animal* animal) {animal->speak(); // 动态绑定
}
class Base {
public:virtual void f1() {}virtual void f2() {}int x;
};class Derived : public Base {
public:void f1() override {}virtual void f3() {}int y;
};
虚表结构示例
对于上面的类,虚表结构大致如下:
Derived 对象:
+---------------+
| vptr | → Derived 的虚表
+---------------+
| Base::x |
+---------------+
| Derived::y |
+---------------+Derived 的虚表:
+---------------+
| Derived::f1() | // 覆盖基类
+---------------+
| Base::f2() | // 继承基类
+---------------+
| Derived::f3() | // 新增虚函数
+---------------+
1. 虚表的构建规则
-
继承的虚函数:派生类会继承基类虚表中的所有条目
-
重写的虚函数:覆盖对应槽位的函数指针
-
新增的虚函数:追加到虚表末尾
2.虚析构函数的必要性
class Base {
public:~Base() {} // 非虚析构函数
};class Derived : public Base {
public:std::vector<int> data;~Derived() {}
};Base* p = new Derived();
delete p; // 未定义行为!Derived 的析构函数不会被调用
正确做法:基类析构函数应该声明为 virtual。
为什么构造函数不可以是虚函数
构造顺序问题
-
构造函数的主要职责是初始化对象的内存和建立对象的类型信息
-
在构造函数执行之前,对象的内存空间刚刚分配,还没有形成有效的对象
-
虚函数调用依赖于虚表指针(vptr),而vptr本身需要在构造函数中初始化
-
对象内存分配后,vptr初始化为0或未定义状态
-
进入构造函数时,编译器首先初始化vptr指向当前类的虚表
-
然后才执行构造函数体内的用户代码
-
在构造函数的成员初始化列表中,vptr已经指向当前类的虚表
总结
虚表(vtable)是编译器为每个包含虚函数的类生成的静态函数指针数组,存储该类所有虚函数的地址。每个对象通过内部的虚指针(vptr)指向其所属类的虚表。虚表在编译期生成,存放在程序的只读数据段,构造函数不能声明为虚函数,因为在对象构造期间vptr尚未完全初始化。而析构函数通常需要声明为虚函数,以确保通过基类指针删除派生类对象时能正确调用完整的析构链,避免资源泄漏。