【C++】多态进阶

1. 多态实现原理探究

1.1 包含虚函数的类的大小

求sizeof(Base)的大小?

class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
private:
	int _b = 1;
};

正常来说,Base类中只用一个int型成员变量,其大小应该是4字节,但是经过VS2013编译后显示,其实是 8字节

我使用VS2013中的监视功能发现:Base类的实例化变量中多了一个void**指针变量_vfptr,这个指针指向的东西是一个虚函数表

image-20210216102431547

1.2 虚函数表

  • 一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表
  • 对象中的这个指针_vfptr我们叫做虚函数表指针(v代表virtual,f代表function)

image-20210216103016815

  • 通过汇编代码可以发现,_vfptr中保存的是虚拟函数的地址

观察以下代码

// 1.我们增加一个派生类Derive去继承Base
// 2.Derive中重写Func1
// 3.Base再增加一个虚函数Func2和一个普通函数Func3
class Base
{
public:
	virtual void Func1(){}
	virtual void Func2(){}
	void Func3(){}
private:
	int _b = 1;
};

class Derive : public Base
{
public:
	virtual void Func1(){}
private:
	int _d = 2;
};

int main()
{
	Base b;
	Derive d;
    
	return 0;
}

image-20210216103945340

黄色框框:我们可以发现,每个对象中有自己独立的虚函数表指针

红色框框:我们可以发现,子类重写父类的虚函数后,重写后的虚函数Func1就是一个新的虚函数了

蓝色框框:我们可以发现。子类继承了父类的虚拟函数Func2,子类并没有重写该函数,故子类对象的虚函数表指针中存放的依然是父类的虚函数Func2的地址

image-20210216132559844


给Derive类加入一个虚拟函数Func4后发现在VS2013的监视的_vfptr中并未显示出Func4的函数入口地址,但是在_vfptr的地址中保存着三个虚拟函数地址

image-20210216135037070

【总结】

  • 虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr
  • 派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
  • 虚函数和普通函数一样都是存在于代码段

1.3 动态绑定与静态绑定

  • 静态绑定:称为前期绑定早绑定,在程序编译期间就确定了程序的行为,称为静态多态,例如:函数重载
  • 动态绑定:称为后期绑定晚绑定,在程序运行期间才确定的程序的行为,称为动态多态
class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
};

class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }
};

void Func(Person& p)
{
	p.BuyTicket();
}

int main()
{
	Person Mike;
	Func(Mike);

	Student Johnson;
	Func(Johnson);

	return 0;
}

输出

买票-全价
买票-半价

汇编代码解释

// 以下汇编代码中跟你这个问题不相关的都被去掉了
void Func(Person* p)
{
	p->BuyTicket();
	
	// p中存的是mike对象的指针,将p移动到eax中
	001940DE mov eax,dword ptr [p]
	// [eax]就是取eax值指向的内容,这里相当于把mike对象头4个字节(虚表指针)移动到了edx
	001940E1 mov edx,dword ptr [eax]
	// [edx]就是取edx值指向的内容,这里相当于把虚表中的头4字节存的虚函数指针移动到了eax
	00B823EE mov eax,dword ptr [edx]
	// call eax中存虚函数的指针。这里可以看出满足多态的调用,不是在编译时确定的,是运行起来以后到对象的中取找的。
	001940EA call eax
	001940EC cmp esi,esp
}
int main()
{
...
	// 首先BuyTicket虽然是虚函数,但是mike是对象,不满足多态的条件,所以这里是普通函数的调用转换成地址时,是在编译时已经从符号表确认了函数的地址,直接call 地址
	mike.BuyTicket();
	00195182 lea ecx,[mike]
	00195185 call Person::BuyTicket (01914F6h)
...
}

2. 单继承中的虚函数表

取出类中的 虚函数表

class Base {
public:
	virtual void func1() { cout << "Base::func1" << endl; }
	virtual void func2() { cout << "Base::func2" << endl; }
private:
	int a;
};
class Derive :public Base {
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
	virtual void func4() { cout << "Derive::func4" << endl; }
private:
	int b;
};
typedef void(*VFPTR) ();
void PrintVTable(VFPTR vTable[])
{
	// 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数
	cout << " 虚表地址>" << vTable << endl;
	for (int i = 0; vTable[i] != nullptr; ++i)
	{
		printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
		VFPTR f = vTable[i];
		f();
	}
	cout << endl;
}
int main()
{
	Base b;
	Derive d;
		// 思路:取出b、d对象的头4bytes,就是虚表的指针,前面我们说了虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr
		// 1.先取b的地址,强转成一个int*的指针
		// 2.再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针
		// 3.再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。
		// 4.虚表指针传递给PrintVTable进行打印虚表
		// 5.需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最后面没有放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的 - 生成 - 清理解决方案,再编译就好了。
	VFPTR* vTableb = (VFPTR*)(*(int*)&b);
	PrintVTable(vTableb);

	VFPTR* vTabled = (VFPTR*)(*(int*)&d);
	PrintVTable(vTabled);

	return 0;
}

输出:

 虚表地址>00BFDC74
 第0个虚函数地址 :0Xbf100a,->Base::func1
 第1个虚函数地址 :0Xbf1285,->Base::func2

 虚表地址>00BFDCA4
 第0个虚函数地址 :0Xbf11ae,->Derive::func1
 第1个虚函数地址 :0Xbf1285,->Base::func2
 第2个虚函数地址 :0Xbf11fe,->Derive::func3
 第3个虚函数地址 :0Xbf111d,->Derive::func4

图解单继承中虚函数表

image-20210216162954846

3. 多继承中的虚函数表

class Base1 {
public:
	virtual void func1() { cout << "Base1::func1" << endl; }
	virtual void func2() { cout << "Base1::func2" << endl; }
private:
	int b1;
};
class Base2 {
public:
	virtual void func1() { cout << "Base2::func1" << endl; }
	virtual void func2() { cout << "Base2::func2" << endl; }
private:
	int b2;
};
class Derive : public Base1, public Base2 {
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
private:
	int d1;
};

typedef void(*VFPTR) ();

void PrintVTable(VFPTR vTable[])
{
	cout << " 虚表地址>" << vTable << endl;
	for (int i = 0; vTable[i] != nullptr; ++i)
	{
		printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);
		VFPTR f = vTable[i];
		f();
	}
	cout << endl;
}

int main()
{
	Derive d;

	VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);
	PrintVTable(vTableb1);

	VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));
	PrintVTable(vTableb2);

	return 0;
}

输出:

 虚表地址>00D9DCD4
 第0个虚函数地址 :0Xd911ae,->Derive::func1
 第1个虚函数地址 :0Xd913f2,->Base1::func2
 第2个虚函数地址 :0Xd911fe,->Derive::func3

 虚表地址>00D9DCE8
 第0个虚函数地址 :0Xd91249,->Derive::func1
 第1个虚函数地址 :0Xd91311,->Base2::func2

图解多继承中虚函数表

在这里插入图片描述

  • 6
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值