【C++】多态的底层原理(虚函数表)


前言

一、虚函数表

在这里插入图片描述
在这里插入图片描述

通过观察测试我们发现b对象是8bytes,除了_b成员,还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数
的地址要被放到虚函数表中
,虚函数表也简称虚表

二、派生类中虚函数表

1.原理

class Base
{
    
    
public:
	virtual void Func1()
	{
    
    
		cout << "Base::Func1()" << endl;
	}
	virtual void Func2()
	{
    
    
		cout << "Base::Func2()" << endl;
	}
	void Func3()
	{
    
    
		cout << "Base::Func3()" << endl;
	}
private:
	int _b = 1;
};
class Derive : public Base
{
    
    
public:
	virtual void Func1()
	{
    
    
		cout << "Derive::Func1()" << endl;
	}
private:
	int _d = 2;
};
int main()
{
    
    
	Base b;
	Derive d;
	return 0;
}

在这里插入图片描述

1.基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
2.另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表。
3.虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。

总结一下派生类的虚表生成a. 先将基类中的虚表内容拷贝一份到派生类虚表中 b. 如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c. 派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

2.例子:

在这里插入图片描述

  1. 观察下图的红色箭头我们看到,p是指向mike对象时,p->BuyTicket在mike的虚表中找到虚函数是Person::BuyTicket。
  2. 观察下图的蓝色箭头我们看到,p是指向johnson对象时,p->BuyTicket在johson的虚表中找到虚函数是Student::BuyTicket。
  3. 这样就实现出了不同对象去完成同一行为时,展现出不同的形态。
    在这里插入图片描述

满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中取找的。不满足多态的函数调用时编译时确认好的。

三、虚函数的存放位置

虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针

同类型共用虚函数表:
由于同一类的不同对象的虚函数表是相同的,它们所需的虚函数的地址是一样的,因此可以共用同一份虚函数表,这样就节省了内存空间。这种设计可以使得每个对象只需要一个指针来指向虚函数表,而不需要为每个对象都分配一份独立的虚函数表。

四 、单继承中的虚函数表

class Person {
    
    
	public:
		virtual void Func1() {
    
    cout << "Person::Func1()" << endl;}
		virtual void Func2() {
    
    cout << "Person::Func2()" << endl;}

		int _a = 0;
	};
class Student : public Person {
    
    
  
private:
	virtual void Func3(){
    
    cout << "Student::Func3()" << endl;}
	protected:
		int _b = 1;
};

	int main() {
    
    
		Person p;
		Student s;
		return 0;
	}

我们会发现虚函数表中少了一个func3的函数指针,我们下面会验证func3确实存在与派生类的虚基表中。
在这里插入图片描述
每个对象都有一个自己的虚函数指针,指向相应的虚函数表。这里基类和派生类的虚函数指针指向同一个地址,是因为基类的虚函数在派生类中没有被重写(Func1与Func2)当派生类继承基类时,派生类会继承基类的虚函数表,并在其虚函数表中添加或覆盖自己的虚函数。如果派生类没有重写基类的虚函数,那么派生类的虚函数表中对应的虚函数地址仍然指向基类中的虚函数。
在这里插入图片描述
虚函数表本质是一个存虚函数指针的指针数组,这个数组最后面放了一个nullptr

思路:取出ps、st对象的头4bytes,就是虚表的指针,然后利用虚表的指针对虚函数数组进行遍历,若能打印出Func3的函数指针,就说明Func3在虚函数表中

typedef void(*FUNC_PTR) ();//FUNC_PTR为函数指针
void PrintVFT(FUNC_PTR* table)//table为函数指针数组
{
    
    
	//虚函数表本质是一个存虚函数
	//指针的指针数组,这个数组最后面放了一个nullpt
	for (size_t i = 0; table[i] != nullptr; i++)
	{
    
       printf("[%d]:%p->", i, table[i]);
		FUNC_PTR f = table[i];
		f();//函数指针指向函数的内存地址,
		//通过调用函数指针,实际上是调用了指向的函数
	}
	printf("\n");
}
	int main() {
    
    
	Person ps;
	Student st;
//1.先取ps的地址,强转成一个int * 的指针
// 2.再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针(二重函数指针)
	int vft1 = *((int*)&ps);
	PrintVFT((FUNC_PTR*)vft1);
//3.再强转成VFPTR * ,因为虚表就是一个存FUNC_PTR类型(虚函数指针类型)的数组。
// 4.虚表指针传递给PrintVTable进行打印虚表
	int vft2 = *((int*)&st);
	PrintVFT((FUNC_PTR*)vft2);
//5.需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最
//后面没有放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的 - 生成 - 清理解决方案,再编译就好了。
		return 0;
	}

在这里插入图片描述

五、多继承中的虚函数表

多继承的对象虚函数表

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

多继承中的虚函数表有多个

多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中
在这里插入图片描述

六、问答题

  1. inline函数可以是虚函数吗?
    答:可以,不过编译器就忽略inline属性,这个函数就不再是inline,因为虚函数要放到虚表中去
    2.静态成员可以是虚函数吗?
    答:不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
    3.构造函数可以是虚函数吗?
    答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的
    4.虚函数表是在什么阶段生成的,存在哪的?
    答:虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。
  2. 对象访问普通函数快还是虚函数更快?
    答:首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。

猜你喜欢

转载自blog.csdn.net/m0_74774759/article/details/132163391