当前位置:   article > 正文

C++ 虚函数,虚函数表_c++虚函数表

c++虚函数表

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

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

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

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

我们通过一个实例观察:

#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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

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

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)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

我们看到,通过类的对象调用函数时,不管是构造函数还是虚函数,都是先传入一个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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

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

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]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

我们可以清晰地看到,调用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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

我们可以看到程序输出窗口输出了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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

我们看到程序输出窗口还是输出了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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31

注意这里我们给对象中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]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

我们着重观察调用两个虚函数的指令: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;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47

我们在父类中定义了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]
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

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

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/在线问答5/article/detail/950541
推荐阅读
相关标签
  

闽ICP备14008679号