C++进阶:多态(六)

49 阅读4分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第13天,点击查看活动详情

💦 模拟虚函数表

✔ 测试用例一:

#include<iostream>
using namespace std;

class Base
{
public:
	virtual void Func1()
	{
		cout << "Base::Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "Base::Func2()" << endl;
	}
 	void Func3()
 	{
 		cout << "Base::Func3()" << endl;
 	}
private:
	int _b = 1;
};
class Derive : public Base
{
public:
	virtual void Func1()
 	{
 		cout << "Derive::Func1()" << endl;
 	}
 	virtual void Func4()
 	{
 		cout << "Derive::Func4()" << endl;
 	}
private:
	int _d = 2;
};

//typedef void(*)() VFPTR//err
typedef void(*VFPTR)();//VFPTR是函数指针,它指向无参的函数,返回值是void*

//打印虚表
void PrintVFT(void* vft[])//虚函数表是一个函数指针数组,不是说函数指针必须是类型一样的才能玩,因为就算类型不一样,也可以使用void*
{
	printf("%p\n", vft);
	for(size_t i = 0; vft[i] != nullptr; i++)//虚表最后会放nullptr
	{
		//printf("vft[%d]: %p\n", i, vft[i]);//打印虚函数

		printf("vft[%d]: %p->", i, vft[i]);//打印虚函数
		//vft[i]();//err,要调用函数指针,就必须是函数指针类型
		VFPTR f = (VFPTR)vft[i];//将虚函数的地址的类型void*强转为函数指针类型,赋值给f
		f();//调用这个虚函数,VS有些版本这里会标红提示,不用理它
	}
	printf("\n");
}


//typedef void(*VFPTR)();

//void PrintVFT(VFPTR vft[])//函数指针数组
//{
//	printf("%p\n", vft);
//	for(size_t i = 0; vft[i] != nullptr; i++)
//	{
//		printf("vft[%d]: %p->", i, vft[i]);	
//		vft[i]();//ok,因为与函数指针类型匹配
//	}
//	printf("\n");
//}

int main()
{
	Base b;
	Derive d;

	PrintVFT((void**)*(int*)&b);//取头上4个字节:&b是bash*;*(int*)&b是bash*到int*的强转,再解引用就是4个字节;(void**)(*((int*)&b))是对头4个字节强转void**,为了与形参类型匹配;如果是64位要取8个字节,就是longlong*
	PrintVFT((void**)*(int*)&d);
	//PrintVFT((void**)*(int)&b);//err,bash*到int的强转,不能取到头4个字节,这里崩溃了,因为这里&b是对象的地址,你只是说把它强转为int,它还是&b的地址,待会对这块空间解引用找虚函数,而这块空间并没有虚函数。

	//PrintVFT((VFPTR*)*(int*)&b);//强转为函数指针
	//PrintVFT((VFPTR*)*(int*)&d);

	return 0;
}
  • 这里基类的 Func1 和 Func2 是虚函数,Func3 是普通函数;派生类重写了基类的 Func1,自己增加了 Func4 的虚函数。但是我们通过监视窗口并没有看到派生类对象中的 Func4,不能确定 VS 在设计这块时是有意的还是无意的,不过大概率猜测是有意的,VS 觉得派生类中新增的虚函数在监视中展现出来也没什么用,所以就没展示,我们也一再的说过监视窗口不一定真实,监视窗口也并没有保证要给你看到原生内存是什么样子,所以不能全依赖它。

    在这里插入图片描述

  • 这里需要补充一个细节是虚函数表本质是一个存储虚函数指针的指针数组,所以一般情况这个数组最后面放了一个 nullptr,这不是标准规定的,但是大多数编译器都是这样实现的。

  • 基于上面的问题,这里我们就模拟一下虚函数表,把虚函数的地址打印出来,其实这个过程就类似模拟着编译器调用虚函数。

  • 运行程序打印了一大串地址,完全对不上,这里其实是编译器的 bug,可能是由于不断的改代码,导致虚表中的 nullptr 加上。

    在这里插入图片描述

    解决方法就是清理解决方案,重新编译。这里我们通过监视窗口验证的同时发现恶心的是验证不了 Func4,虽然我们能猜到它就是 Func4。

    在这里插入图片描述

    我们发现这些函数的类型都是一样的 void*,我们有函数的地址就可以调用它们,但是 void* 没法调,所以我们要对 void* typedef 为函数指针类型 VFPTR,再用这个函数指针类型定义的变量接收 对 void* 类型强转之后的地址,最后再调用这个函数。这时就一定能确定这个地址就是对应虚函数的地址,因为我调用了它,只有调用到正确的那个虚函数,它才能打印后面的那句话出来。

    在这里插入图片描述

✔ 测试用例二:

#include<iostream>
using namespace std;

class Base
{
public:
	virtual void Func1()
	{
		cout << "Base::Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "Base::Func2()" << endl;
	}
	void Func3()
	{
		cout << "Base::Func3()" << endl;
	}
private:
	int _b = 1;
};
class Derive : public Base
{
public:
	virtual void Func1()
	{
		cout << "Derive::Func1()" << endl;
	}
	virtual void Func4()
	{
		cout << "Derive::Func4()" << endl;
	}
private:
	int _d = 2;
};

typedef void(*VFPTR)();
void PrintVFT(void* vft[])
{
	printf("%p\n", vft);
	for (size_t i = 0; vft[i] != nullptr; i++)
	{
		printf("vft[%d]: %p->", i, vft[i]);
		VFPTR f = (VFPTR)vft[i];
		f();
	}
	printf("\n");
}

int main()
{
	Base bb;

	int a = 0;
	int* p1 = new int;
	const char* p2 = "dancebit";
	auto pf = PrintVFT;
	static int b = 1;

	printf("栈帧变量: %p\n", &a);
	printf("堆变量: %p\n", p1);
	printf("常量区变量: %p\n", p2);
	printf("函数地址变量: %p\n", pf);
	printf("静态区变量: %p\n", &b);
	printf("虚函数表地址: %p\n", *(int*)(&bb));

	return 0;
}
  • 虚函数存在哪 ?虚表存在哪 ❓

      很多人都会深以为然的认为虚函数存在虚表,虚表存在对象。但其实虚函数不存在对象,对象里存的是一个虚表的指针,虚函数编译出来的函数指令同普通函数一样,存在代码段,只是虚函数的地址又被放到了虚表中。而关于虚表存在哪,我们这里采用一种比较粗糙的验证方式,通过虚函数表地址与其它内存区域的地址进行比对,最终我们认为虚函数表是在常量区或代码段。

    在这里插入图片描述