C++ 虚函数,虚函数表

如果基类与派生类中有同名成员函数,根据类型兼容规则,当使用基类指针或基类引用操作派生类对象时,只能调用基类的同名函数。如果想要使用基类指针或基类引用派生类中的成员函数,就需要虚函数解决,虚函数是实现多态的基础。

一.虚函数简介,函数的直接调用和间接调用

虚函数的声明方式是在成员函数的返回值类型前面加上virtual关键字,格式如下:

class 类名{
权限控制符:
	virtual 函数返回值类型 函数名(参数表);
	其他成员...
};

我们通过一个实例观察:

#include "stdafx.h"

class Base{
public:
	void Function_1(){
		printf("Function_1...\n");
	}
	virtual void Function_2(){
		printf("Function_2...");
	}
};

int main(int argc, char* argv[])
{
	Base base;
	base.Function_1();
	base.Function_2();
	return 0;
}

我们来到反汇编观察这两个函数的调用过程:

base.Function_1();
8D 4D FC             lea         ecx,[ebp-4]
E8 06 FF FF FF       call        @ILT+25(Base::Function_1) (0040101e)
base.Function_2();
8D 4D FC             lea         ecx,[ebp-4]
E8 F9 FE FF FF       call        @ILT+20(Base::Function_2) (00401019)

我们看到,通过类的对象调用函数时,不管是构造函数还是虚函数,都是先传入一个this指针,然后通过call的方式调用函数(硬编码为E8)。
我们再来通过指针来调用函数看看:

#include "stdafx.h"

class Base{
public:
	void Function_1(){
		printf("Function_1...\n");
	}
	virtual void Function_2(){
		printf("Function_2...");
	}
};

int main(int argc, char* argv[])
{
	Base base;
	Base* p;
	p->Function_1();
	p->Function_2();
	return 0;
}

我们来到反汇编查看函数调用过程:

p->Function_1();
8B 4D F8             mov         ecx,dword ptr [ebp-8]
E8 06 FF FF FF       call        @ILT+25(Base::Function_1) (0040101e)
p->Function_2();
8B 45 F8             mov         eax,dword ptr [ebp-8]
8B 10                mov         edx,dword ptr [eax]
8B F4                mov         esi,esp
8B 4D F8             mov         ecx,dword ptr [ebp-8]
FF 12                call        dword ptr [edx]

我们可以清晰地看到,调用Function_1函数的时候是通过call指令调用的,而调用Function_2的时候是通过call一个地址来调用的,而且硬编码为FF。
这里就是我们介绍的间接调用和直接调用:**直接调用为直接call函数地址,硬编码为E8,间接调用是先call一个地址,通过地址中存储的值再调用函数,硬编码为FF,这个有点像我们之前PE里面讲的IAT表的作用。

二.深入了解虚函数调用(虚函数表)

我们通过sizeof函数来看看虚函数是否占用类的空间:
我们先来写一个虚函数看看:

#include "stdafx.h"

class Base{
public:
	int a;
	int b;
	void Function_1(){
		printf("Function_1...\n");
	}
	virtual void Function(){
	printf("Function_2...\n");
	}
};

int main(int argc, char* argv[])
{
	printf("%d",sizeof(Base));
	return 0;
}

我们可以看到程序输出窗口输出了12,而我们的类中本来就有两个int类型数据,我们之前讲过构造函数是不占用类的内存的,所以证明虚函数占用了类中4个字节空间。
我们再来写两个虚函数试试:

#include "stdafx.h"

class Base{
public:
	void Function_1(){
		printf("Function_1...\n");
	}
	virtual void Function_2(){
		printf("Function_2...\n");
	}
	virtual void Function_3(){
		printf("Function_3...\n");
	}

int main(int argc, char* argv[])
{
	printf("%d",sizeof(Base));
	return 0;
}

我们看到程序输出窗口还是输出了12,说明不管是几个虚函数,都只占用类中4个字节空间。
那么多出来这四个字节空间到底是什么?
我们通过反汇编查看:
这里通过指针分别调用了两个虚函数,更好地理解

#include "stdafx.h"

class Base{
public:
	int a;
	int b;
	Base(int a,int b){
		this->a = a;
		this->b = b;
	}
	void Function_1(){
		printf("Function_1...\n");
	}
	virtual void Function_2(){
		printf("Function_2...\n");
	}
	virtual void Function_3(){
		printf("Function_3...\n");
	}
};

int main(int argc, char* argv[])
{
	Base* p;
	Base base(1,2);
	p = (Base*)&base;
	p->Function_1();
	p->Function_2();
	p->Function_3();
	return 0;
}

注意这里我们给对象中a和b赋了初值,我们来到反汇编窗口来观察一下函数调用过程:

p->Function_1();
8B 4D FC             mov         ecx,dword ptr [ebp-4]
E8 26 38 FF FF       call        @ILT+35(Base::Function_1) (00401028)
p->Function_2();
8B 4D FC             mov         ecx,dword ptr [ebp-4]
8B 11                mov         edx,dword ptr [ecx]
8B F4                mov         esi,esp
8B 4D FC             mov         ecx,dword ptr [ebp-4]
FF 12                call        dword ptr [edx]
3B F4                cmp         esi,esp
E8 2B 39 FF FF       call        __chkesp (00401140)
p->Function_3();
8B 45 FC             mov         eax,dword ptr [ebp-4]
8B 10                mov         edx,dword ptr [eax]
8B F4                mov         esi,esp
8B 4D FC             mov         ecx,dword ptr [ebp-4]
FF 52 04             call        dword ptr [edx+4]

我们着重观察调用两个虚函数的指令:FF 12 call dword ptr [edx] FF 52 04 call dword ptr [edx+4]我们可以发现它俩的函数地址好像只差了4个字节。
我们来到Memory窗口来看看:
类的内存

我们可以观察到,调用虚函数的时候,将this指针指向的第一个内容当作地址,然后取出该地址中的值,通过调用这个地址,完成了调用函数。
我们给出一张图,相信大家能够更好地理解:
虚函数表
在这里我们的this指针指向类,第一个成员为虚函数表,之后的是类的属性,在虚函数表中,函数的排序是按照我们定义虚函数的顺序排列的。

四.子类函数的虚函数表

我们来创建一个父类,再创建一个子类,子类继承父类的函数,那么在子类的虚函数表中,是怎样排序的呢?
我们来试验一下:

#include "stdafx.h"
           
class Base{
public:
	int a;
	int b;
	/*Base(int a,int b){
		this->a = a;
		this->b = b;
	}*/
	void Function_1(){
		printf("Function_1...\n");
	}
	virtual void Function_2(){
		printf("Function_2...\n");
	}
	virtual void Function_3(){
		printf("Function_3...\n");
	}
};

class Base2:public Base{
public:
	int c;
	int d;
	Base2(int a,int b,int c,int d){
		this->a = a;
		this->b = b;
		this->c = c;
		this->d = d;
	}
	virtual void Function_4(){
		printf("Function_4...\n");
	}
};

int main(int argc, char* argv[])
{
	Base2* p;
	Base2 ase(1,2,3,4);
	p = (Base2*)&ase;
	p->Function_1();
	p->Function_2();
	p->Function_3();
	p->Function_4();
	return 0;
}

我们在父类中定义了Function_1,Function_2,Function_3函数,其中Function_2,Function_3为虚函数
创建子类,继承父类,并定义一个虚函数Function_4,我们来看看调用这些虚函数的过程:
我们通过反汇编来查看:

p->Function_1();
8B 4D FC             mov         ecx,dword ptr [ebp-4]
E8 84 FF FF FF       call        @ILT+5(Base::Function_1) (0040100a)
p->Function_2();
8B 4D FC             mov         ecx,dword ptr [ebp-4]
8B 11                mov         edx,dword ptr [ecx]
8B F4                mov         esi,esp
8B 4D FC             mov         ecx,dword ptr [ebp-4]
FF 12                call        dword ptr [edx]
3B F4                cmp         esi,esp
E8 D7 01 00 00       call        __chkesp (00401270)
p->Function_3();
8B 45 FC             mov         eax,dword ptr [ebp-4]
8B 10                mov         edx,dword ptr [eax]
8B F4                mov         esi,esp
8B 4D FC             mov         ecx,dword ptr [ebp-4]
FF 52 04             call        dword ptr [edx+4]
3B F4                cmp         esi,esp
E8 C3 01 00 00       call        __chkesp (00401270)
p->Function_4();
8B 45 FC             mov         eax,dword ptr [ebp-4]
8B 10                mov         edx,dword ptr [eax]
8B F4                mov         esi,esp
8B 4D FC             mov         ecx,dword ptr [ebp-4]
FF 52 08             call        dword ptr [edx+8]

首先我们能够看到,不管是调用父类虚函数还是子类虚函数,都是通过虚函数表间接调用的,我们来看看子类this指针中的情况:
this指针
我们仍然能够看到,this第一个成员为虚函数表地址,后面的是子类属性的值。
我们再来看看虚函数表的情况:
虚函数表
结合前面的返回编,我们可以得出结论:
子类虚函数表中的排序:父类的虚函数在前,并且按照定义的顺序排序,之后才是子类的虚函数(也是按照定义的顺序排序)。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Shad0w-2023

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

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

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

打赏作者

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

抵扣说明:

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

余额充值