C++多态原理,单继承和多继承关系的虚函数表详解

多态的原理

虚函数表
在这里插入图片描述
在vs编译器中,可以看出来a对象除了有一个_a成员,还多一个__vfptr放在对象的前面,对象中的这个指针叫做虚函数表指针。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中, 虚函数表也简称虚表。

派生类虚表构建规则

  1. 将基类虚表中的内容拷贝一份放到子类虚表中。
  2. 如果派生类重写了基类某个虚函数,用派生类自己的虚函数替换原先基类虚函数的入口地址(重写)。
  3. 如果派生类增加了新的虚函数且有多个虚表(多重继承),将会把新的虚函数地址增加到第一个虚表中,按照其在类中声明次序依次增加到虚表的最后。
  4. 虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。

多态的原理
在这里插入图片描述

  • 通过观察a,b对象,可以看到对象a在虚表中找到的是虚函数A::Func1(void),对象b在虚表中找到的是虚函数B::Func1(void)。
  • 这样就实现了多态,不同对象去完成同一行为时,展现出不同的形态。
  • 这也正是构成多态的二个必要条件的原因所在。1.虚函数的覆盖,只有重写了基类的虚函数,才能通过虚函数表,找到子类对基类虚函数的实现。2.通过基类对象的指针或者引用调用虚函数。
  • 满足多态的函数调用,不是在编译时确定的,是运行起来以后到对象的虚表中去找的,不满足多态的函数调用是编译时确定好的。
int main()
{
	B b;
	A* pa = &b;
	
	//通过虚表
	pa->Func1();
	//直接调用
	b.Func1();
	
	return 0}

在这里插入图片描述
虽然调用的都是同一个函数,但是我们可以明显看出来汇编代码是明显不同的。通过基类指针调用函数是通过虚表找到函数地址的。而直接调用该函数,虽然该函数是虚函数,但是不满足多态的条件,所以这里是普通函数的调用过程,在编译时就确定了函数的地址,直接call地址。

动态绑定与静态绑定

  1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数 重载
  2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。

单继承和多继承关系的虚函数表

单继承中的虚函数表

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; 
};
  • 在vs编译器中,通过监视窗口发现看不见func3和func4。这里可以认为是编译器的一个小bug。

在这里插入图片描述

  • 但是我们通过查看内存,就可以看出来虚表中明显多了两个地址。
    在这里插入图片描述
  • 也可以使用代码打印出虚表中的函数。
typedef void(*VFPTR)();//虚函数指针
void Print(VFPTR *arr)
{
	for (int i = 0; arr[i] != nullptr; ++i)
	{
		//printf("第%d个虚函数地址:%x\n", i, arr[i]);
		arr[i]();
	}
}
int main()
{
	Derive d;
	VFPTR* arr = (VFPTR*)(*(int*)&d);//虚表指针(虚函数指针数组)
	Print(arr);
	return 0;
}
  • 我们可以打印虚函数地址,也可以调用虚函数。
    在这里插入图片描述

多继承中的虚函数表

class Base1 { 
public:    
	virtual void func1() {cout << "Base1::func1" << endl;}    
	virtual void func2() {cout << "Base1::func2" << endl;} 
private:    
	int b1; 
};
 
class 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(*VFPTR)();//虚函数指针
void Print(VFPTR *arr)
{
	for (int i = 0; arr[i] != nullptr; ++i)
	{
		printf("第%d个虚函数地址:%0x\n",i, arr[i]);
		arr[i]();
	}
}
int main()
{
	Derive d;
	VFPTR* arr1 = (VFPTR*)(*(int*)&d);//虚表指针(虚函数指针数组)
	VFPTR* arr2 = (VFPTR*)(*(((int*)&d)+2));//第二个虚表指针偏移量
	Print(arr1);
	Print(arr2);
	return 0;
}	
  • 如下图,d对象是有两个虚表的。

在这里插入图片描述

  • 我们将第一个虚表地址所在的地址加上偏移量即可求得第二个虚表地址的地址。将他们虚表内容打印出来或者运行虚函数。
    在这里插入图片描述
  • 可以看出多继承派生类的新增加的虚函数放在第一个继承基类部分的虚函数表中
发布了161 篇原创文章 · 获赞 52 · 访问量 5万+

猜你喜欢

转载自blog.csdn.net/qq_42837885/article/details/102985347