C++虚函数与虚函数表(V-Table)深度解析
C++ 通过虚函数实现了动态多态,其核心依赖于虚函数表(V-Table)。本文将深入解析虚函数的定义规则、虚函数表的结构以及在单一继承和多重继承场景下的表现,并通过实际代码展示如何通过内存地址遍历虚函数表,从而彻底打通 C++ 多态的运行机理。
1. 虚函数的基本定义与规则
C++ 中的虚函数是实现多态的关键机制。简而言之,就是使用父类指针指向子类实例,并通过父类指针调用实际的子类成员函数。这种“一种接口,多种形态”的技术被称为泛型技术。
第1步:定义虚函数
目的:创建一个可以在派生类中被重新定义的基类函数。
操作:在基类的非静态成员函数声明前添加 virtual 关键字。
注意:访问权限通常设为 public,但私有或保护的虚函数依然存在并被继承,只是无法直接通过外部访问。
class Base {
public:
virtual void func() { /* ... */ }
};
第2步:确认动态联编的三个条件
要实现真正的运行时多态,必须同时满足以下条件:
- 行为必须是公共的虚函数。
- 类之间存在公有派生的子类型关系。
- 使用基类指针或引用指向派生类对象,并调用该虚函数。
第3步:了解虚函数的限制
- 不能是静态成员函数:静态函数不依赖对象实例,而虚函数表属于对象实例的一部分,二者矛盾。
- 不能是构造函数:构造过程尚未完成对象初始化,此时虚函数表指针
vptr尚未正确建立,且构造函数无法被重载(多态的前提)。 - 析构函数建议设为虚函数:当通过基类指针删除派生类对象时,若析构函数不是虚函数,只会调用基类析构函数,导致派生类资源泄漏。
特殊说明:在构造函数或析构函数内部调用虚函数时,多态机制不起作用。构造函数中因为派生类部分尚未构建;析构函数中因为派生类部分已经销毁。此时编译器会将其视为普通成员函数调用。
2. 虚函数表(V-Table)结构分析
C++ 标准规定,包含虚函数的类在其实例对象的内存起始位置存放一个指向虚函数表的指针(通常称为 vptr)。虚函数表是一张存储了该类所有虚函数地址的数组。这张表解决了继承和覆盖问题,确保通过基类指针调用时能正确匹配到实际的函数。
我们可以通过强制类型转换来获取和查看虚函数表。假设有一个简单的基类:
class Base {
public:
virtual void f() { cout << "Base::f" << endl; }
virtual void g() { cout << "Base::g" << endl; }
virtual void h() { cout << "Base::h" << endl; }
};
通过以下代码可以获取虚表地址及第一个虚函数地址:
typedef void(*Fun)(void);
Base b;
Fun pFun = NULL;
// 获取虚函数表地址
cout << "虚函数表地址:" << (int*)(&b) << endl;
// 获取表中第一个函数地址
cout << "虚函数表 — 第一个函数地址:" << (int*)*(int*)(&b) << endl;
// 调用第一个虚函数
pFun = (Fun)*((int*)*(int*)(&b));
pFun();
上述代码执行后,输出将显示具体的内存地址及打印出 “Base::f”。这验证了虚函数表指针位于对象起始位置,且可以通过索引偏移访问后续的虚函数地址。例如,要调用 g() 和 h(),只需在表指针基础上加 1 和 2:
(Fun)*((int*)*(int*)(&b)+1); // Base::g()
(Fun)*((int*)*(int*)(&b)+2); // Base::h()
无虚函数覆盖的继承
当派生类没有重写任何父类虚函数时,派生类的虚函数表包含了父类的虚函数地址以及自身新增的虚函数地址。父类虚函数排在前面。
在此继承关系中,子类没有重写任何父类的函数。那么,在派生类的实例的虚函数表如下所示:
有虚函数覆盖的继承
如果子类重写了父类的虚函数,子类的虚函数表中,原父类虚函数的位置会被子类重写的函数地址所替换,未被重写的函数则保持原样。这就是多态实现的本质。
为了让大家看到被继承过后的效果,在这个类的设计中,我只覆盖了父类的一个函数:f()。那么,对于派生类的实例的虚函数表会是下面的样子:
对于下面这样的程序,由
Base *b = new Derive(); b->f();
3. 多重继承中的虚函数表
在多重继承中,每个父类都有自己的虚函数表。派生类会在内存中放置多个 vptr,分别对应不同基类的视图。派生类的虚函数会被放置在第一个基类的虚函数表末尾(按基类声明顺序)。
无覆盖的多重继承
假设有三个基类 Base1, Base2, Base3 和一个继承它们的派生类 Derive。由于 Derive 没有覆盖任何函数,其虚表结构如下:
对于子类实例中的虚函数表,是下面这个样子:
我们可以看到:
1) 每个父类都有自己的虚表。 2) 子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的) 这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。
有覆盖的多重继承
如果在派生类中覆盖了某个父类的虚函数,那么在对应的父类虚表槽位中,地址会被更新为派生类的函数地址。其他未覆盖的父类虚表保持不变。
下面是对于子类实例中的虚函数表的图:
我们可以看见,三个父类虚函数表中的f()的位置被替换成了子类的函数指针。这样,我们就可以用任一个父类指针来指向子类,并调用子类的f()了。如:
Derive d; Base1 *b1 = &d; Base2 *b2 = &d; Base3 *b3 = &d; b1->f(); //Derive::f() b2->f(); //Derive::f() b3->f(); //Derive::f() b1->g(); //Base1::g() b2->g(); //Base2::g() b3->g(); //Base3::g()
4. 安全性与潜在风险
虽然我们通过正规途径无法绕过访问控制,但从理论上看,虚函数表的存在带来了一些特殊的访问可能性:
- 访问子类特有的虚函数:即使通过父类指针,理论上可以通过计算虚表偏移量找到子类独有的虚函数地址并强行调用,但这违反了 C++ 的类型安全语义,极易导致程序崩溃。
- 访问私有/保护虚函数:私有的虚函数同样存在于虚函数表中。通过指针算术运算获取其地址并调用,可以绕过
private或protected的限制。这在某些极端调试或逆向场景中可见,但在正常开发中属于危险行为。
class Base {
private:
virtual void f() { cout << "Base::f" << endl; }
};
class Derive : public Base{ };
typedef void(*Fun)(void);
void main() {
Derive d;
Fun pFun = (Fun)*((int*)*(int*)(&d)+0);
pFun(); // 成功输出 Base::f
}
5. 总结
C++ 是一门需要深入理解底层的语言。虚函数表是 C++ 多态的基石,它使得编译期不确定运行期的具体函数行为成为可能。掌握虚函数表的布局规律,有助于我们更好地设计继承体系、排查内存相关 bug,并理解面向对象设计的性能开销。
附录:VC 中查看虚函数表的方法
在 Visual Studio 的 Debug 模式下,展开类的实例变量,即可在调试器窗口中看到对应的虚函数表指针及其内容。
附录二:多重继承虚函数表访问完整例程
#include <iostream>
using namespace std;
class Base1 {
public:
virtual void f() { cout << "Base1::f" << endl; }
virtual void g() { cout << "Base1::g" << endl; }
virtual void h() { cout << "Base1::h" << endl; }
};
class Base2 {
public:
virtual void f() { cout << "Base2::f" << endl; }
virtual void g() { cout << "Base2::g" << endl; }
virtual void h() { cout << "Base2::h" << endl; }
};
class Base3 {
public:
virtual void f() { cout << "Base3::f" << endl; }
virtual void g() { cout << "Base3::g" << endl; }
virtual void h() { cout << "Base3::h" << endl; }
};
class Derive : public Base1, public Base2, public Base3 {
public:
virtual void f() { cout << "Derive::f" << endl; }
virtual void g1() { cout << "Derive::g1" << endl; }
};
typedef void(*Fun)(void);
int main()
{
Fun pFun = NULL;
Derive d;
int** pVtab = (int**)&d;
// Base1's vtable
pFun = (Fun)pVtab[0][0]; pFun();
pFun = (Fun)pVtab[0][1]; pFun();
pFun = (Fun)pVtab[0][2]; pFun();
pFun = (Fun)pVtab[0][3]; pFun(); // Derive's specific function
pFun = (Fun)pVtab[0][4]; cout<<pFun<<endl; // Tail
// Base2's vtable
pFun = (Fun)pVtab[1][0]; pFun();
pFun = (Fun)pVtab[1][1]; pFun();
pFun = (Fun)pVtab[1][2]; pFun();
pFun = (Fun)pVtab[1][3]; cout<<pFun<<endl;
// Base3's vtable
pFun = (Fun)pVtab[2][0]; pFun();
pFun = (Fun)pVtab[2][1]; pFun();
pFun = (Fun)pVtab[2][2]; pFun();
pFun = (Fun)pVtab[2][3]; cout<<pFun<<endl;
return 0;
}
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













