深入分析虚函数表

问题一:

(1)一个类只有包含虚函数才会存在虚函数表,同属于一个类的对象共享虚函数表,但是有各自的vptr(虚函数表指针),当然所指向的虚函数表是同一个。

(2)父类中有虚函数就等于子类中有虚函数。只要父类中有虚函数,子类中即便不写virtual,也依旧是虚函数。

(3)不管是父类还是子类,都会只有一个虚函数表。不能认为子类继承父类,就认为子类的虚函数表就是,父类的虚函数表+子类的虚函数表。这样的想法是不正确的。子类就只会只有子类的虚函数表这一个表。那么子类中是否可能存在多个虚函数表呢?当然可以存在的,比如子类存在多个父类,然后这个父类也继承于另外一个父类(这个类中存在一个虚函数)。

(4)如果子类中完全没有新的虚函数,则我们可以认为子类的虚函数表和父类的虚函数表内容相同,但,仅仅是内容相同,这两个表在内存中处于不同的位置。换句话来说,这是内容相同的两张表。虚函数表中的每一项,保存着一个虚函数的首地址,但如果子类的虚函数某项和父类的虚函数表某项代表同一个函数。(这表示子类没有覆盖父类的虚函数)

(5)超出虚函数表部分内容不可知。

问题二:

(1)虚函数表指针(vptr)是什么时候创建出来的?vptr跟着对象走(虚函数表跟着类走),所以对象什么时候创建出来,虚函数表指针就什么时候创建出来。实际上,对于这种有虚函数的类,在编译的时候,编译器会往相关的构造函数中增加为vptr赋值的代码,这是在编译期间编译器为构造函数所做的事情。(这个就解释了为什么构造函数和析构函数中不能发生多态,因为在执行构造函数的时候,对象没有诞生,虚函数表指针也没诞生。在执行析构函数的时候,虚函数表指针已经被摧毁,所以更不可能发生多态。)

(2)虚函数表是什么时候创建的?虚函数表是编译器在编译期间(不是运行期间)就为每个类确定好了对应的虚函数表vtbl的内容。

问题三:

下面我们来看一个程序例子。

#include <iostream>

using namespace std;

class X
{
public:
	int x;
	int y;
	int z;
	X() //:x(0), y(0), z(0)
	{
		memset(this,0,sizeof(X));
		cout << "构造函数被执行" << endl;
	}
	X(const X& tm) //:x(tm.x), y(tm.y), z(tm.z)
	{
		memcpy(this,&tm,sizeof(X));
		cout << "拷贝构造函数被执行" << endl;
	}
};

int main()
{
	X x0;

	x0.x = 100;
	x0.y = 200;
	x0.z = 300;

	X x1(x0);

	cout << "x1.x=" << x1.x << endl;
	cout << "x1.y=" << x1.y << endl;
	cout << "x1.z=" << x1.z << endl;

	return 0;
}

分析上述程序:在构造函数中用memset(this,0,sizeof(X))代替构造函数中的初始化列表方式进行初始化。以及在拷贝构造函数中采用memcpy(this,0,sizeof(X))来代替初始化列表进行初始化。这样的代替方式看似没有任何问题。但是如果类不单纯,那么在构造函数中使用memset或者在拷贝构造函数中使用如上的memcpy方法,就会出现程序崩溃。

不单纯的类:在某些情况下,编译器会往类内部增加一些我们看不见但真实存在的成员变量(隐藏成员变量)。有了这种变量的类,就不单纯了。同时这种隐藏的成员变量的增加(使用)或者赋值的时机,往往都是在执行构造函数或者拷贝构造函数的函数体之前进行。那么这个时候如果使用memset,memcpy,很可能把编译器给隐藏变量的值你就给清空了,要么覆盖了。比如,在类中增加了虚函数,系统默认往类对象中增加虚函数表指针,这个虚函数表指针就是隐藏的成员变量。(本来虚函数表指针是要正确的指向类的虚函数表的,但是这个时候用memset把它清零,这个时候肯定会发生程序崩溃。)

下面来看一个比较有意思的问题:

#include <iostream>

using namespace std;

class X
{
public:
	int x;
	int y;
	int z;
	X() //:x(0), y(0), z(0)
	{
		memset(this,0,sizeof(X));
		cout << "构造函数被执行" << endl;
	}
	X(const X& tm) //:x(tm.x), y(tm.y), z(tm.z)
	{
		memcpy(this,&tm,sizeof(X));
		cout << "拷贝构造函数被执行" << endl;
	}
	virtual ~X()
	{
		cout << "析构函数被执行了" << endl;
	}
	virtual void virfunc()
	{
		cout << "虚函数virfunc()被执行了" << endl;
	}
	void ptfunc()
	{
		cout << "普通函数ptfunc()被执行了" << endl;
	}
};

int main()
{
	X x0;

	/*x0.x = 100;
	x0.y = 200;
	x0.z = 300;*/
	x0.virfunc();

	/*X x1(x0);

	cout << "x1.x=" << x1.x << endl;
	cout << "x1.y=" << x1.y << endl;
	cout << "x1.z=" << x1.z << endl;*/

	//X* px0 = new X();
	//px0->ptfunc();//普通成员函数可以正常调用
	//px0->virfunc(); //无法正常调用,产生异常
	//delete px0;   //无法正常调用,产生异常
	new出来的对象,所有的虚函数无法正常调用

	return 0;
}
//在G++和VS2017下都能正常的输出为:
构造函数被执行
虚函数virfunc()被执行了
析构函数被执行了

分析上述代码:

1.此时的虚函数表指针已经为nullptr,但是在main()函数中在栈上面定义了一个局部对象,x0,然后通过x0去调用虚函数居然正常的输出。是不是感觉很奇怪呢?我们知道虚函数的调用过程是:通过对象的虚函数表指针->找到类的虚函数表->在虚函数中找到对应的虚函数首地址->最后去调用相应的虚函数。但是此时,对象的虚函数表指针已经为nullptr,又怎么能够调用相应的虚函数呢?奇怪!!!

2.如果我们不在栈上面创建对象,而是在堆上创建对象上述例子中有对应的代码:

X* px0 = new X();
px0->ptfunc();//普通成员函数可以正常调用
px0->virfunc(); //无法正常调用,产生异常
delete px0;   //无法正常调用,产生异常

这个时候就发生异常了,只要涉及到虚函数就不能够正常调用了,是不是很奇怪呢?在解释这个原因之前,我们在来看一个例子。

#include <iostream>

using namespace std;

class X
{
public:
	int x;
	int y;
	int z;
	X() //:x(0), y(0), z(0)
	{
		memset(this,0,sizeof(X));
		cout << "构造函数被执行" << endl;
	}
	X(const X& tm) //:x(tm.x), y(tm.y), z(tm.z)
	{
		memcpy(this,&tm,sizeof(X));
		cout << "拷贝构造函数被执行" << endl;
	}
	virtual ~X()
	{
		cout << "析构函数被执行了" << endl;
	}
	virtual void virfunc()
	{
		cout << "虚函数virfunc()被执行了" << endl;
	}
	void ptfunc()
	{
		cout << "普通函数ptfunc()被执行了" << endl;
	}
};

int main()
{
	//X x0;

	/*x0.x = 100;
	x0.y = 200;
	x0.z = 300;*/
	//x0.virfunc();

	/*X x1(x0);

	cout << "x1.x=" << x1.x << endl;
	cout << "x1.y=" << x1.y << endl;
	cout << "x1.z=" << x1.z << endl;*/

	//X* px0 = new X();
	//px0->ptfunc();//普通成员函数可以正常调用
	//px0->virfunc(); //无法正常调用,产生异常
	//delete px0;   //无法正常调用,产生异常
	//new出来的对象,所有的虚函数无法正常调用

	int i = 9;
	printf("i的地址=%p\n",&i);
	X x0;
	printf("ptfunc()的地址=%p\n",&X::ptfunc);
	//当如果不用memset的时候,可以打印出虚函数的地址
	//long* pvptrpar = (long*)(&x0);
	//long* vptrpar = (long*)(*pvptrpar);
	//printf("virfunc的地址=%p\n", vptrpar[1]);//虚函数virfunc的地址,vptrpar[0]是虚析构函数~X()的地址

	x0.ptfunc();
	x0.virfunc();

	return 0;
}

分析上述程序:

(1)运行上述程序,我们可以看出每次运行之后i的地址都会发生变化,但是普通成员函数ptfunc(如果没有memset的时候,虚函数virfunc)都不变,大家深入思考一下这个是什么原因呢?为了验证这个原因,我们来看一下,x0.ptfunc();x0.virfunc();这两句代码的汇编代码,如下所示:

    x0.ptfunc();
00BD6593  lea         ecx,[x0]  
00BD6596  call        X::ptfunc (0BD14D8h)  
    x0.virfunc();
00BD659B  lea         ecx,[x0]  
00BD659E  call        X::virfunc (0BD14D3h)

调用普通成员函数或者调用虚函数,是直接在对应的函数地址处调用执行。所以这个函数ptfunc()和virfunc()函数,是在编译期间就是确定的。这个就是我们常说的静态联编。所谓的静态联编:在程序编译期间就能够确定调用哪个函数,给每个函数都分配了确定的地址,把调用语句和调用函数绑定了到一起。而动态联编:是与静态联编相反,是在程序运行时,根据实际情况,动态的把调用语句和被调函数绑定到一起,也就是说只有到了程序运行期间,才能够确定调用哪个函数。动态联编一般只有在动态和虚函数的情况下才存在。所以这个就解释了为什么在虚函数表指针被清0之后,还能够成功的调用虚函数了。就是因为这个是静态联编,在程序编译的期间就可以确定每个函数的调用地址,并不需要虚函数表指针,虚函数表等。

我们在来看一下另外一种情况,在堆空间中new出来的对象。

X* px0 = new X();
px0->ptfunc();//普通成员函数可以正常调用
px0->virfunc(); //无法正常调用,产生异常
delete px0;   //无法正常调用,产生异常

以上四行程序对应的汇编:

X* px0 = new X();
01132A6D  mov         dword ptr [px0],ecx  
px0->ptfunc();//普通成员函数可以正常调用,静态联编
01132A70  mov         ecx,dword ptr [px0]  
01132A73  call        X::ptfunc (011314D8h)  
 px0->virfunc(); //无法正常调用,产生异常,动态联编
01132A78  mov         eax,dword ptr [px0]  
01132A7B  mov         edx,dword ptr [eax]  
01132A7D  mov         esi,esp  
01132A7F  mov         ecx,dword ptr [px0]  
01132A82  mov         eax,dword ptr [edx+4]  
01132A85  call        eax  
01132A87  cmp         esi,esp  
01132A89  call        __RTC_CheckEsp (011312E9h)  
delete px0;   //无法正常调用,产生异常
01132A8E  mov         eax,dword ptr [px0]  
01132A91  mov         dword ptr [ebp-104h],eax  
01132A97  mov         ecx,dword ptr [ebp-104h]  
01132A9D  mov         dword ptr [ebp-0F8h],ecx  
01132AA3  cmp         dword ptr [ebp-0F8h],0  
01132AAA  je          main+101h (01132AD1h)  
01132AAC  mov         esi,esp  
01132AAE  push        1  
01132AB0  mov         edx,dword ptr [ebp-0F8h]  
01132AB6  mov         eax,dword ptr [edx]  
01132AB8  mov         ecx,dword ptr [ebp-0F8h]  
01132ABE  mov         edx,dword ptr [eax]  
01132AC0  call        edx  
01132AC2  cmp         esi,esp  
01132AC4  call        __RTC_CheckEsp (011312E9h)  
01132AC9  mov         dword ptr [ebp-10Ch],eax  
01132ACF  jmp         main+10Bh (01132ADBh)  
01132AD1  mov         dword ptr [ebp-10Ch],0  

分析以上汇编程序:px0->virfunc(); 这行程序是动态联编,通过虚函数表指针,找到虚函数表,然后从虚函数表中找到virfunc虚函数的地址并调用。但是不幸的是我们在构造函数用用memset把虚函数表指针清0了,所以程序就会崩溃。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

repinkply

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

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

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

打赏作者

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

抵扣说明:

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

余额充值