浅析虚函数的vptr和虚函数表

浅析虚函数的vptr和虚函数表

前言

​ 为了实现虚函数,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会访问越界报错,可能是我这边的问题,恳请点拨!

在这里插入图片描述

  • 9
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

螺蛳粉只吃炸蛋的走风

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值