C++多态调用实现原理(虚函数表详解)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/shanghx_123/article/details/82973791

1.带有虚函数的基类对象模型

我们先看段代码:

#include<iostream>
using namespace std;
class B1
{
	public:
		void func1()
		{}
		int _b;
};
class B2
{
	public:
		virtual void func()
		{}
		int _b;
};
int main()
{
	cout<<"sizeof(B1) = "<<sizeof(B1)<<endl;  
	cout<<"sizeof(B2) = "<<sizeof(B2)<<endl;
	system("pause");
	return 0;
}

运行结果:
在这里插入图片描述
可以看出,B2的这个类比B1多了4个字节,而这4个字节就是用来存放虚函数的地址,也就是说,这4个字节的数据是一个指针(地址),这个指针指向的是虚函数地址。看个图就很容易理解。
在这里插入图片描述
我们需要注意的是:
1.B2对象的前4个字节存放的是虚表的地址,其后才是B2该对象的成员变量;(虚函数表我们也叫做虚表)。
2.若B2这个类中有多个虚函数,那么其对象大小还是8,因为前4个字节是存放虚函数表的地址,在这个虚函数表(函数指针数组)里面每个元素才是每个虚函数的地址。

2.派生类对象虚函数表如何构建?

上个例子,只是给出了带有虚函数基类的对象模型,那派生类的对象虚函数表应该如何构建?

#include<iostream>
#include<string>
using namespace std;
class Base					//基类
{
public:
	virtual void TestFunc1()
	{
		cout << "Base::TestFunc1()" << endl;
	}
	virtual void TestFunc2()
	{
		cout << "Base::TestFunc2()" << endl;
	}
	virtual void TestFunc3()
	{
		cout << "Base::TestFunc3()" << endl;
	}
	int _b;
};
class Derived : public Base					//派生类
{
public:
	virtual void TestFunc4()
	{
		cout << "Derived::TestFunc4()" << endl;
	}
	virtual void TestFunc1()
	{
		cout << "Derived::TestFunc1()" << endl;
	}

	virtual void TestFunc3()
	{
		cout << "Derived::TestFunc3()" << endl;
	}
	virtual void TestFunc5()
	{
		cout << "Derived::TestFunc5()" << endl;
	}
	int _d;
};
typedef void(*PVFT)();				//声明函数指针,用来调用虚函数

void PrintVFT(Base& b, const string& str)
{
	cout << str << endl;
	/*这里是先将对象的地址取出来再强制类型换,此时再解引用的话,取的值就是对象前四个字节地址里面存放的值,
	这个值就是虚表的地址,即就是函数指针数组的首地址,我们再将这个地址转换成函数指针类型*/
	PVFT* pVFT = (PVFT*)(*(int*)&b)while (*pVFT)					//虚表中最后一个元素是空值,打印完循环退出
	{
		(*pVFT)();			// 再解引用就是函数指针数组里面的第一个元素(即就是第一个虚函数地址),再往后一次打印
		++pVFT;
	}
	cout << endl;
}
void TestVirtualFunc(Base& b)
{
	b.TestFunc1();
	b.TestFunc3();
	return;
}
int main()
{
	Base b;
	Derived d;
	// 打印基类与派生类的虚表
	PrintVFT(b, "Base VFT:");
	PrintVFT(d, "Derived VFT:");
	// 传递Base类对象
	TestVirtualFunc(b);
	cout << endl;
	// 传递派生类对象
	TestVirtualFunc(d);
	system("pause");
	return 0;
}

运行结果:
在这里插入图片描述
根据结果,我们首先可以看出基类虚函数表是按照声明顺序依此存放,派生类的虚函数表则相应的发生了一些改变。
1.先将基类中的虚表内容拷贝一份到派生类虚表中
2.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数替换虚表中基类的虚函数
3.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后

其实我们没必要刻意去记这些规则,如果用多态的思想去考虑下的话,这样的规则很合理。对我自己而言,首先派生类继承基类,那么基类有的东西,派生类本来没有的东西继承之后也就具有;其次,若基类和派生类都具有相同的东西,那么在派生类的虚表中,我派生类要保持我自己的特点,所以此时派生类的虚表中存放的是自己的虚函数,这样做的目的很简单,就是为了在多态调用时,会很灵活,根据对象本身自己来调用相应的虚函数,假如派生类中的虚表中存放的是基类的虚函数,那么请问,在多态调用时,不管给基类还是派生类的对象,调用的虚函数都是级基类的虚函数,这样做还会实现多态这个特性吗?最后一点不言而喻,派生类独有的虚函数按照声明顺序加在虚表后面即可。
讨论完基类和派生类对象的虚表之后,我们来看看派生类对象完整的对象模型。

3.单继承的派生类对象模型

对于单继承的派生类的对象模型,其和我们上面介绍的第一个例子一样,前四个字节是虚表的地址,后面就是按顺序存放
接着看一段代码:

#include<iostream>
using namespace std;

class Base
{
public:
	virtual void func1()
	{
		cout << "Base::func1" << endl;
	}

	virtual void func2()
	{
		cout << "Base::func2" << endl;
	}
private:
	int a;
};

class Derive :public Base
{
public:
	virtual void func1()
	{
		cout << "Derive::func1" << endl;
	}

	virtual void func3()
	{
		cout << "Derive::func3" << endl;
	}

	virtual void func4()
	{
		cout << "Derive::func4" << endl;
	}
private:
	int b;
};

typedef void(*VFUNC)();

void PrintVTbale(int* table)
{
	printf("vtable:%p\n", table);
	int i = 0;
	
	for (int i = 0; table[i]!= NULL; ++i) //虚表最后一个元素为空值
	{	
		printf("table[%d]:%p->", i, table[i]);
		VFUNC f = (VFUNC)table[i];	//转为函数指针
		f();	//调用对应的虚函数
	}
	return;
}

运行结果:
在这里插入图片描述
根据结果:基类的对象模型和上面第一个例子一样,不再过多讨论。对于单继承的派生类对象模型,在我们清楚了其虚表的构建之后,再其虚函数表地址后面按顺序先存放基类的对象,其次再存放派生类的对象即可。
派生类的对象模型那就是在虚函数指针后面,按顺序先存放基类的成员变量,接着再存放派生类自己成员变量。如下图:
在这里插入图片描述
下面我们来看多继承的派生类的对象模型

4.多继承的派生类对象模型

看代码:

#include<iostream>
using namespace std;

class Base1				//基类Base1
{
public:
	virtual void func1()
	{
		cout << "Base1::func1" << endl;
	}
	virtual void func2()
	{
		cout << "Base1::func2" << endl;
	}
private:
	int b1;
};

class Base2				//基类Base2
{	
public:
	virtual void func1()
	{
		cout << "Base2::func1" << endl;
	}
	virtual void func2()
	{
		cout << "Base2::func2" << endl;
	}
private:
	int b2;
};

class Derive : public Base1, public Base2		//派生类
{
public:
	virtual void func1() {
		cout << "Derive::func1" << endl;
	}
	virtual void func3()
	{
		cout << "Derive::func3" << endl;
	}
private:
	int d1;
};

typedef void(*VFUNC)();

void PrintVTbale(int* table)
{
	printf("vtable:%p\n", table);
	for (int i = 0; table[i] != 0; ++i)
	{
		printf("table[%d]:%p->", i, table[i]);
		VFUNC f = (VFUNC)table[i];
		f();
	}
	cout << endl;
}

int main()
{
	Base1 b1;
	Base2 b2;

	Derive d;
	cout << sizeof(d) << endl;		//计算派生类对象的大小

	PrintVTbale((int*)(*(int*)&b1));
	PrintVTbale((int*)(*(int*)&b2));

	PrintVTbale((int*)(*(int*)&d));		//第一个虚表
	PrintVTbale((int*)(*(int*)((char*)&d + sizeof(Base1))));	//第二个虚表
	system("pause");
	return 0;
}

运行结果:

在这里插入图片描述
我们注意到;派生类对象的大小是20,对于多继承的派生类对象,如果只有一个虚表,那么它的大小应该是4(虚表指针) + 4(b1)+ 4(b2) + 4(d1) = 16,可是结果不是16,所以派生类对象的虚表应该不止一个。
所以他的对象模型应该如下图这样:
在这里插入图片描述
总结以下就是:
1.对于多继承的派生类的对象,其不但继承了基类的对象,也继承了基类的虚函数表指针;
2.派生类继承多个基类,派生类的对象模型其实就相当于将基类的对象模型继承下来了,只不过对于派生类独有的虚函数,那么他的虚函数指针将会按顺序存放在第一个虚表中最后的位置。
3.最后再加上派生类自己成员

至此,对于常见的普通类型多态调用就这些,还有其他继承类型的多态调用,如菱形继承等。
后面会再做总结。

注:文中如有不正之处,欢迎指出,希望和大家共同学习,进步。

猜你喜欢

转载自blog.csdn.net/shanghx_123/article/details/82973791