浅析虚函数的vptr和虚函数表
文章目录
- 浅析虚函数的vptr和虚函数表
- 前言
- 1. 基础理论
- 2. 实现与内部结构
前言
为了实现虚函数,C++使用一种称为虚拟表的特殊形式的后期绑定。该虚拟表是用于解决在动态/后期绑定方式的函数调用函数的查找表。虚拟表有时会使用其他名称,例如 “vtable”,“虚函数表”,“虚方法表” 或 “调度表”。本文就实际案例出发,对虚表指针和虚函数表的模型进行刻画。
1. 基础理论
首先,每个使用虚函数的类(或者从使用虚函数的类派生)都有自己的虚拟表。该表只是编译器在编译时设置的静态数组。虚拟表包含可由类的对象调用的每个虚函数的一个条目。此表中的每个条目只是一个函数指针,指向该类可访问的派生函数。
其次,编译器还会添加一个隐藏指向基类的指针,我们称之为 vptr。vptr在创建类实例时自动设置,以便指向该类的虚拟表。与this指针不同,this指针实际上是编译器用来解析自引用的函数参数,vptr是一个真正的指针。
因此,vptr使每个类对象的分配大一个指针的大小。这也意味着vptr由派生类继承,这很重要。
2. 实现与内部结构
下面我们来看自动与手动操纵vptr来获取地址与调用虚函数!
开始看代码之前,为了方便理解,这里给出调用图:
#include <iostream>
using namespace std;
/**
* @brief 函数指针
*/
typedef void (*Fun)();
/**
* @brief 基类
*/
class Base
{
public:
Base() {};
virtual void fun1() { cout << "Base::fun1()" << endl; }
virtual void fun2() { cout << "Base::fun2()" << endl; }
virtual void fun3() {} // 注意虚函数fun3()未被子类重写
~Base() {};
};
/**
* @brief 派生类
*/
class Derived : public Base
{
public:
Derived() {};
void fun1() { cout << "Derived::fun1()" << endl; }
void fun2() { cout << "DerivedClass::fun2()" << endl; }
~Derived() {};
};
/**
* @brief
* 获取vptr地址与func地址,vptr指向的是一块内存,这块内存存放的是虚函数地址,这块内存就是我们所说的虚表
*
* @param obj
* @param offset
*
* @return
*/
Fun getAddr(void* obj, unsigned int offset)
{
cout << "=======================" << endl;
void* vptr_addr =
(void*)*(unsigned long*)obj; // 64位操作系统,占8字节,通过*(unsigned
// long *)obj取出前8字节,即vptr指针
printf("vptr_addr:%p\n", vptr_addr);
/*
* @brief 通过vptr指针访问virtual
* table,因为虚表中每个元素(虚函数指针)在64位编译器下是8个字节,因此通过*(unsigned
* long *)vptr_addr取出前8字节, 后面加上偏移量就是每个函数的地址!
*/
void* func_addr = (void*)*((unsigned long*)vptr_addr + offset);
printf("func_addr:%p\n", func_addr);
return (Fun)func_addr;
}
int main(void)
{
Base ptr;
Derived d;
Base* pt = new Derived(); // 基类指针指向派生类实例
Base& pp = ptr; // 基类引用指向基类实例
Base& p = d; // 基类引用指向派生类实例
cout << "基类对象直接调用" << endl;
ptr.fun1();
cout << "基类引用指向基类实例" << endl;
pp.fun1();
cout << "基类指针指向派生类实例并调用虚函数" << endl;
pt->fun1();
cout << "基类引用指向派生类实例并调用虚函数" << endl;
p.fun1();
// 手动查找vptr 和 vtable
Fun f1 = getAddr(pt, 0);
(*f1)();
Fun f2 = getAddr(pt, 1);
(*f2)();
delete pt;
return 0;
}
运行结果:
基类对象直接调用
Base::fun1()
基类引用指向基类实例
Base::fun1()
基类指针指向派生类实例并调用虚函数
Derived::fun1()
基类引用指向派生类实例并调用虚函数
Derived::fun1()
=======================
vptr_addr:00EBAB68
func_addr:00EB1217
Derived::fun1()
=======================
vptr_addr:00EBAB68
func_addr:00EB126C
DerivedClass::fun2()
对应的打开反汇编界面,查询Derived::fun1()
和DerivedClass::fun2()
的地址:
通过对比观察发现,我们通过程序读取到的虚函数调用地址与汇编中定义的类内虚函数地址一致!
注:这段代码在x86环境下正常运行如上,但x64会访问越界报错,可能是我这边的问题,恳请点拨!