深度探索C++对象模型(8)——虚函数(1)——虚函数表指针的位置和虚函数表分析

1.虚函数表指针的位置分析

一个类有虚函数的话,会产生一个虚函数表

而生成这个类的对象的时候,这个对象就会产生一个指针,指向虚函数表的开始位置,这个指针就是虚函数指针(vptr),有种类似于这个对象的成员变量,它是占字节数的,如在vs2017中占4个字节,在Linux下占8个字节

而vptr在对象内存中的位置取决于编译器

分析虚函数表指针的位置分析的代码:

#include <iostream>
using namespace std;

class A
{
public:
	int i;   //                                   vs2017:4个字节         Linux下g++中:4个字节
	virtual void testfunc() {}  //虚函数,vptr     vs2017:4个字节         Linux下g++中:8个字节
};

int main()
{
	A a_obj;
	int a_obj_len = sizeof(a_obj);
	int i_len = sizeof(a_obj.i);
	cout << "a_obj_len=" << a_obj_len << endl;  
	cout << "i_len=" << i_len << endl;//4个字节

	char *p1 = reinterpret_cast<char *>(&a_obj); //类型转换,硬转 &a_obj这是对象aobj的首地址。
	char *p2 = reinterpret_cast<char *>(&(a_obj.i));
	if (p1 == p2) //说明aobj.i和aobj的位置相同,说明i在对象aobj内存布局的上边。虚函数表指针vptr在下边
	{
		cout << "虚函数表指针位于对象内存的末尾" << endl;
	}
	else
	{
		cout << "虚函数表指针位于对象内存的开头" << endl;
	}

	return 1;
}

VS2017中运行结果:

Linux下g++编译运行的结果:

注意:在Linux下,虚函数表指针为8个字节,int为4个字节,而该程序执行结果为16个字节的原因为字节对齐,对象内存总大小必须为最长变量的整数倍

上述结果说明在VS和Linux的g++编译器中,虚函数表指针都位于对象内存的开头

大致如图:

2.继承关系作用下虚函数的手工调用

#include <iostream>
using namespace std;

//父类
class Base
{
public:
	virtual void f() { cout << "Base::f()" << endl; }
	virtual void g() { cout << "Base::g()" << endl; }
	virtual void h() { cout << "Base::h()" << endl; }
};

//子类
class Derive :public Base
{
	virtual void g() { cout << "Derive::g()" << endl; }  //覆盖虚函数g()
	//int r;
};


int main()
{
	//输出两个类的内存大小
	cout << sizeof(Base) << endl;		
	cout << sizeof(Derive) << endl;

	//通过派生类对象转换为它相应的
	Derive *d = new Derive(); //派生类指针。
	long *pvptr = (long *)d;  //指向对象的指针d转成了long *类型。
	long *vptr = (long *)(*pvptr); //(*pvptr) 表示pvptr指向的对象,也就是Derive本身。Derive对象是4字节的,
代表的是虚函数表指针地址。
	 //此处由于函数的定义中只有虚函数,所以Derive对象内存中的内容就是虚函数表指针,

	//long *vptr = (long *)(pvptr[0]);  //比如如果有其他的成员占用内存,就需要用指针索引来访问第一个大小的值
	//(因为这里内存是进行了字节对齐的,所以指针索引一次访问一个最大成员长度的值,
	//而访问第一个是因为虚函数表指针在VS和Linux下的位置都在对象内存的开头)					 
	
	cout << "---------Base-----------" << endl;

	//输出每个函数调用的内存
	for (int i = 0; i <= 5; i++) //循环5次;
	{
		printf("vptr[%d] = 0x:%p\n", i, vptr[i]);
	}



	//使用虚函数表指针访问对应虚函数然后调用
	typedef void(*Func)(void); //定义一个函数指针类型
	Func f = (Func)vptr[0]; //f就是函数指针变量。 vptr[0]是指向第一个虚函数的。
	Func g = (Func)vptr[1];
	Func h = (Func)vptr[2];

	f();
	g();
	h();

	cout << "---------Base-----------" << endl;
	//同上
	Base *dpar = new Base();
	long *pvptrpar = (long *)dpar;
	long *vptrpar = (long *)(*pvptrpar);

	for (int i = 0; i <= 4; i++) //循环5次;
	{
		printf("vptr Base[%d] = 0x:%p\n", i, vptrpar[i]);
	}

	Func fpar = (Func)vptrpar[0];
	Func gpar = (Func)vptrpar[1];
	Func hpar = (Func)vptrpar[2];

	
	fpar();
	gpar();
	hpar();

	return 1;
}

运行结果:

解释:

1)两个4是虚函数表指针的大小

2)接下来都是通过虚函数表指针访问相应虚函数存储地址和调用虚函数的结果

具体一些其他解释,见代码中注释

图示理解:

问题:子类覆盖g()之后,原来父类中的虚函数还在吗?

个人理解:

每个类有自己的虚函数表,

父类的虚函数表中有f(),g(),h()
而子类的虚函数表为f(),重写的g(),h(),所以在子类的虚函数表中是没有父类的g()的

从上述结果可以发现子类和父类的虚函数表中未被子类覆盖的函数表项指向的内存地址相同,而被覆盖的函数表项指向的内存不同,说明这种覆盖是在内存上新写了子类函数的代码,而不是刷新内存中对应的父类函数代码,所以原来父类中的虚函数的定义还在内存中

只是它的调用需要去父类调用g(),就需要到父类的虚函数表中也就是存储该类的内存块中去找到g(),所以在前面加父类作用域,也就是到父类的内存地址域中去找

3.虚函数表分析

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

(2)父类中有虚函数就等于子类中有虚函数。也就是,父类中有虚函数表,则子类中肯定有虚函数表。

只要在父类中是虚函数,那么子类中即便不写virtual,也依旧是虚函数。不管是父类还是子类,都只会有一个虚函数表

问题:子类中是否可能会有多个虚函数表呢?

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

(4)超出虚函数表部分的内容不可知,即没有意义

代码分析:

#include <iostream>
using namespace std;

//父类
class Base
{
public:
	virtual void f() { cout << "Base::f()" << endl; }
	virtual void g() { cout << "Base::g()" << endl; }
	virtual void h() { cout << "Base::h()" << endl; }
};
class Derive :public Base
{
	virtual void g() { cout << "Derive::g()" << endl; }
};

int main()
{
	typedef void(*Func)(void); //定义一个函数指针类型

	//定义一个子类对象1,通过虚函数表指针调用相应的虚函数
	Derive derive;
	long *pvptrderive = (long *)(&derive);
	long *vptrderive = (long *)(*pvptrderive); //指向类Derive的虚函数表
	Func f1 = (Func)vptrderive[0]; //指向Base::f()
	Func f2 = (Func)vptrderive[1]; //指向Derive::g()
	Func f3 = (Func)vptrderive[2]; //指向Base::h()
	Func f4 = (Func)vptrderive[3]; //非法
	Func f5 = (Func)vptrderive[4]; //非法

	//定义一个子类对象2,通过虚函数表指针调用相应的虚函数
	Derive derive2 = derive; //调用拷贝构造函数
	long *pvptrderive2 = (long *)(&derive2);
	long *vptrderive2 = (long *)(*pvptrderive2);    //此处的虚函数表指针和vprtderive相同



	//定义一个父类对象,用子类赋值给父类对象
	Base base = derive;//直接用子类对象给父类对象值,子类中的属于父类那部分内容会被编译器自动区分(切割)出来并拷贝给了父类对象
		//所以Base base = derive;实际干了两个事情:
			//第一个事情:生成一个base对象
			//第二个事情:用derive来初始化base对象的值。
			//这里编译器给咱们做了一个选择,显然derive初始化base对象的时候,
			//derive的虚函数表指针值并没有覆盖base对象的虚函数表指针值,即父类对象base有不同于子类对象的虚函数表指针,
			//这是编译器帮我们做到了这点;
	long *pvptrbase = (long *)(&base);
	long *vptrbase = (long *)(*pvptrbase); //指向父类Base的虚函数表
	Func fb1 = (Func)vptrbase[0];   //指向Base::f()
	Func fb2 = (Func)vptrbase[1];   //指向Base::g()
	Func fb3 = (Func)vptrbase[2];   //指向Base::h()

	return 1;
}

猜你喜欢

转载自blog.csdn.net/qq_34805255/article/details/86488450