虚函数表和虚函数表指针的汇编分析

1.什么是虚函数?
2.虚函数有什么用?
3.怎样才产生虚函数表?
4.虚函数表存放在什么位置?
5.虚函数表长什么样?
6.为什么用虚函数实现多态时需要用到指针或者引用,而不能直接赋值?

假设有这么一个需求

class Creature
{
    
    
	public:
		int hp;
		int mp;
		char* name;
		void Attack();
		void Defend();
		void Dead();
		Creature(char* name);
};

class Player:public Creature
{
    
    
public:
	int nSmart;
	int nPower;
	void Buy();
	void Dead();
	Player(char* name);
};

父类和子类具有同名函数,void Dead();

void Creature::Dead()
{
    
    
	printf("I am Dead.\n");
}
void Player::Dead()
{
    
    
	printf("game over.\n");
}

我们创建了一个函数去调用父类和子类的void Dead();我们想要这么一个结果,当传入父类对象时,调用父类的同名函数,传入子类对象时调用子类的同名函数。(所谓多态)

void DoSometing(Creature* c)
{
    
    
	c->Dead();
}

至于为什么形参里面是Creature* c而不是Player* p,是由于Player权限(//过大)的指针访问Creature结构体是不安全的,而Creature权限的指针访问Player结构体是安全的(所谓继承的特性)

那为什么不能这样?(问题6),这个问题稍后解决

void DoSometing(Creature c)
{
    
    
	c.Dead();
}

调用DoSometing时的结果

I am Dead.
I am Dead.

由于Creature原则上是指向一个Creature类型的结构体,调用Creature相关的Dead就合情合理,但我们的要求是,让Creature指向Player的结构体时,调用Player相关的Dead就很无理取闹
但由于C++的特性,在父类的同名函数前加上virtual,在子类的同名函数前加上override(不加也行,不过加了的情况下,当要重写的函数名不小心写错了,编译器会报错,起到提醒的作用),那什么是虚函数呢?(问题1)加上virtual关键字的函数就是虚函数,虚函数的作用就是实现我们刚才的那个需求(问题2)

virtual void Dead();
void Dead() override;

现在DoSometing的执行结果就达到预期了

I am Dead.
game over.

那么现在问题是加上关键字后,C++帮我们做了哪些额外的工作来实现这一个需求,那直接看汇编代码

lea ecx,dword ptr ss:[ebp-18] //局部变量Player

Player本身的成员变量只占20字节,但现在居然占了24字节

00EFFB50  00F15B40  @[ñ.  x86test.const Player::`vftable'
00EFFB54  00000064  d...  
00EFFB58  00000032  2...  
00EFFB5C  00F15BD0  Ð[ñ.  "sanqiu"
00EFFB60  0000000A  ....  
00EFFB64  00000007  ....  

Player结构体的第一个成员指向了vftable(虚函数表),所以第一成员又叫做虚函数表指针,所以说,虚函数表的位置可以从结构体第一个成员中找到(问题4),当然,是在结构体存在虚函数表的前提下。后面五个成员跟以前一样

 lea eax,dword ptr ss:[ebp-28] 

Creature本身的成员变量只占12字节,但现在居然占了16字节

00EFFB40  00F15B34  4[ñ.  x86test.const Creature::`vftable'
00EFFB44  00000064  d...  
00EFFB48  00000032  2...  
00EFFB4C  00F15BD0  Ð[ñ.  "sanqiu"

Creature结构体的第一个成员也指向了vftable(虚函数表),后面的三个成员跟以前一样。
所以现在我们明白了,当父类函数有virtual关键字的函数时就会有虚函数表,当子类函数重写父类基函数时也会有虚函数表,那么又产生了一个问题,子类不重写父类的虚函数会怎么样?,答案是,仍然会产生一个虚函数表(问题3)
可以推理得到,即使无论子类是否重写父类的虚函数,都会存在一个虚函数表,但是,这两个虚函数表的内容肯定是不一样(这是肯定的,不然控制台也不会打印出不同的结果了)
那虚函数长什么样呢?(问题5),可以把虚函数表理解为一个以0x00000000结尾的特殊指针字符串(一般的字符串以0x0或0x00结尾),就像这样:函数地址,函数地址,函数地址,0x00000000。当然,在X64程序中就是0x0000000000000000了
当不重写时,Player的虚函数表长这样
函数地址(I am dead的那个函数)
0x00000000
当重写时,Player的虚函数表长这样
函数地址(game over的那个函数)
0x00000000
所以说所谓重写父类虚函数,就是重写从父类继承过来的虚函数表中的对应函数地址!!!
那最后一个问题就是问题6了,解决这个问题需要分析DoSomething里面的代码

void DoSometing(Creature* c)
{
    
    
	c->Dead();
}
关键汇编代码
mov eax,dword ptr ss:[ebp+8]	//eax = 对象指针
mov edx,dword ptr ds:[eax]		//edx = 虚函数表指针
mov ecx,dword ptr ss:[ebp+8]	//ecx = 对象指针 
mov eax,dword ptr ds:[edx]		//eax = 虚函数(I am dead 或者 game over)
call eax						//调用 
								//在汇编下,虚函数的功能一目了然
void DoSometing(Creature c)
{
    
    
	c.Dead();
}
关键汇编代码
lea ecx,dword ptr ss:[ebp+8]
call x86test.B9115E				//C++根本没使用虚函数表,而是直接CALL了 I am dead的那个
								//函数(x86test.B9115E)

现在,问题6也解决完毕了
但是在汇编层面,C++完全有能力做到在传对象的情况下实现多态,因为Creature c 和 Creature *c传递参数的方式完全相同, 但C++最终却不这么做,那就不得而知了

ps:同一个类的对象的虚函数表指针是一样的,也就是都指向同一张虚函数表,这样比较节省内存

猜你喜欢

转载自blog.csdn.net/sanqiuai/article/details/124534961