详细C++三大特性——多态底层原理

目录

一,多态的原理

1.1 虚函数表

1.2 虚函数的重写(覆盖)的底层实现

1.3 子类新建虚函数地址的存放位置

1.4 虚表存放位置

 1.5 多态的原理

1.6 动态绑定与静态绑定

二,多继承

2.1 多继承的虚函数表

 2.2 子类新建虚函数地址的存放位置

2.3 为什么两张虚表中重写的虚函数地址不一样?

 总结


前文

上一篇主要讲了多态的基本内容和使用,本篇文章将带领铁子们深入了解多态的底层原理,本文实验比较多,建议铁子们看完可以自己再实验实验,一定会收货颇丰。

一,多态的原理

1.1 虚函数表

class Person
{
public:
	virtual void Buyticket()
	{
		cout << "全价票" << endl;
	}

	int _a;
};

class Student :public Person
{
public:
	virtual void Buyticket()
	{
		cout << "半价票" << endl;
	}

	int _b;
};
int main()
{
	cout << sizeof(Person) << endl;
	return 0;
}

上面代码老铁们可以算一下Person的空间大小是多少?

答案出来了,是8,我们可以看一下正常没有虚函数的大小

 上图我们发现正常的大小可能和大多数老铁算的一样是4,而有虚函数的是8,那么这多出来的4个字节用到哪里了呢?我们可以调试看一眼

 经过调试观察,我们发现在对象的前面多了一个指针_vfptr,那么这个_vfptr叫做什么呢?这个指针我们叫做虚函数表指针(Virtual Function Pointer),指向虚函数表. 一个含有虚函数的类中至少有一个虚函数表指针指向虚函数表,虚函数的地址会存储在虚函数表中,那么子类的表中有什么呢,我们往下继续看。

观察上面,我们可以发现p和s所指向的虚函数表是不一样的,两者是独立的,子类的虚表中存储着自己重写后的虚函数的地址,这也是多态能够实现的重要原理,因此多态实现的重要条件之二必须通过父类的指针或者引用调用虚函数,大家想必也能够理解,以子类为例,父类的指针或者引用是对子类进行切片,因此指针和引用指向的仍是原来的空间,也因此_vfptr中存储的是子类重写的虚函数,借此可以明确区分子类和父类的虚函数调用

反过来看,为什么不能传值调用呢?如果传值调用,就需要用到赋值重载,正常是不会拷贝虚表的,这样永远都是父类的虚表,多态就无法实现,但是如果我们要拷贝虚表呢,这样多态可能会实现,但是会造成混乱,如下面这种情况,p作为一个父类对象它的虚表是父类还是子类的呢?他会变成子类的,这正常吗,一个父类对象里用着子类虚表,很明显这不正常。

int main()
{
	Person p;
	Student s;

	p = s;

	return 0;
}

接下来我们来看一下为什么虚函数的重写也可以叫做覆盖

1.2 虚函数的重写(覆盖)的底层实现

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

	virtual void Func2()
	{
		cout << "Func2" << endl;
	}

	int _a=1;
};

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

	int _b=2;
};

int main()
{
	Person p;
	Student s;

	return 0;
}

 我们以上面的代码为例,在上面的例子中我们可以看到,子类Studnet只重写了父类Person的Func1虚函数,那么我们进入调试模式看一看,Student和Person的虚表有什么不同。

通过观察上图,我们发现在子类中Func1由于我们重写后,所以父类虚表中Func1和子类虚表中Func1的虚函数地址是不一样的,而Func2子类则没有重写,父类虚表和子类虚表中的Func2的虚函数地址是一样的

这是因为在子类継承父类后,子类对Func1进行了重写,所以函数虚表中父类原来的Func1就被子类重写的Func1的虚函数地址所覆盖

因此虚函数的重写也叫做覆盖,覆盖就是指虚表中虚函数的覆盖,重写是语法的叫法,覆盖是底层原理的叫法

1.3 子类新建虚函数地址的存放位置

讲完重写的原理,接下来我们来探讨一下子类新建虚函数存放在什么位置?是父类的虚表中还是又创建了一个虚表存放?

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


	int _a=1;
};

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

	virtual void Func2()
	{
		cout << "Func2" << endl;
	}

	int _b=2;
};

int main()
{
	Person p;
	Student s;

	return 0;
}

我们以上面代码为例来讲解这个问题,在上面代码中子类对象Studen对父类对象Person的Func1进行了重写,此外呢,我们在子类中又写了一个新的虚函数Func2,借此来观察Func2的存放位置。

我们可以先进行调试来观察虚表中有没有存储Func2,那么Func2难道没有被存储吗?这是不可能的,因为Student也有可能成为别的父类,因此其新建虚函数不可能不存储。

 通过观察监视窗口,我们发现虚表中只存了Func1,真的是这样吗,要知道监视窗口是处理过的,我们可以再观察一下内存窗口

 观察内存窗口,我们发现问题没有这么简单,第一行是Func1的地址,第二行的地址和Func1离得很近,那么这个有没有可能也是一个虚函数的地址?是Func2的地址呢?

接下来我们写一个打印虚表的小程序来证明一下

 

 打印结果如下

 观察打印结果我们发现,第二行的地址确实是Student类的新写的虚函数Func2的地址。

因此我们得出结论子类对象新写的虚函数尽管监视窗口看不到,但确实存到了虚表里面

 那么我们又有一个问题,虚表存放在哪里呢?

1.4 虚表存放位置

内存空间大概分为一下几个区,老铁们可以猜测一下虚表存放在那个区

 这里我们用来试验的方法是,写创建四个变量,分别存于栈,堆,静态区,常量区,然后取其地址,将他们的地址和虚表的地址作对比,借此来推断虚表存储的位置

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


	int _a = 1;
};

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

	virtual void Func2()
	{
		cout << "Func2" << endl;
	}

	int _b = 2;
};

int main()
{
	int i;//栈
	int* ptr = new int;//堆
	static int a = 0;//静态区
	const char* b = "cccccccccc";//常量区

	Person p;
	Student s;

	printf("栈对象:%p\n", &i);
	printf("堆对象:%p\n", ptr);
	printf("静态区对象:%p\n", &a);
	printf("常量区对象:%p\n", b);
	printf("p虚函数表:%p\n", *((int*)&p));
	printf("s虚函数表:%p\n", *((int*)&s));


	return 0;
}

试验结果如下

通过实验,我们发现虚函数表的存储位置和常量区十分接近

由此可得,虚函数表存储的位置和虚函数存储的位置一样在代码段也就是常量区。

 1.5 多态的原理

我们上面讲了这么多。那么多态的原理到底是什么呢?

实际上,多态实现的根本原理就是通过virtual声明存在多态,生成虚表指针,管理虚函数,如果访问的为虚函数,则通过指针/引用找到实际指向的实体,获取实体中的虚表指针,通过虚表指针访问虚表,在虚表中找到需要执行的虚函数指针,通过虚函数指针执行具体函数行为。

1.6 动态绑定与静态绑定

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

二,多继承

上面讲的都是以单继承为例,接下来我们来讲一下多继承的虚函数表

2.1 多继承的虚函数表

在说多继承的虚函数表之前,铁子们可以思考一个问题,多继承的子类中有几张虚表?

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

	int _a=1;
};
class Base2
{
public:
	virtual void Func1()
	{
		cout << "Base2:Func1" << endl;
	}
	virtual void Func2()
	{
		cout << "Base2:Func2" << endl;
	}

	int _b=2;
};

class Son :public Base1, public Base2
{
public:
	virtual void Func1()
	{
		cout << "Son:Func1" << endl;
	}

	int _c = 3;
};

int main()
{
	Son s;
	return 0;
}

我们以上面代码为例子,来看一下多继承的内存模型

 通过观察我们发现多继承内虚函数表有两张,内存模型如下

 2.2 子类新建虚函数地址的存放位置

上面单继承时,子类新建虚函数存放在父类的虚表中,但是这个多继承有两个父类,那么他新建的虚函数地址存在哪里呢。

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

	int _a=1;
};
class Base2
{
public:
	virtual void Func1()
	{
		cout << "Base2:Func1" << endl;
	}
	virtual void Func2()
	{
		cout << "Base2:Func2" << endl;
	}

	int _b=2;
};

class Son :public Base1, public Base2
{
public:
	virtual void Func1()
	{
		cout << "Son:Func1" << endl;
	}


	virtual void Func3()
	{
		cout << "Son:Func3" << endl;
	}


	int _c = 3;
};

typedef void(*VF_PTR)();
//typedef void(*)() VF_PTR;上面定义的效果就和这个类似,
//但是由于是函数指针,所以只能用上面的定义方式
void print(VF_PTR* table)
{
	for (int i = 0; table[i] != nullptr; i++)
	{
		printf("[%d]:%p->", i, table[i]);
		VF_PTR f = table[i];//保存函数地址
		f();//对函数地址调用
	}
	cout << endl;
}

int main()
{
	Son s;


	return 0;
}

这里我们可以用之前打印虚表的方式来验证。但是这里有一个问题,那就是Base2的指针要如何找到呢?我们这里采取的方法是,切片,对Base2切片,指针会自动偏移到Base2的位置。

 由上述结果我们可以发现在多继承中子类新建虚函数的地址存储在第一个父类的虚表中

上述实验结果不知到老铁们有没有发现一个问题,那就是我们在子类中重写了Func1,但是Base1和Base2两张虚表中Func1的地址却不一样,这是为什么?

2.3 为什么两张虚表中重写的虚函数地址不一样?

我们可以将Base1 Base2分别切片出来调用Func1来观察各自汇编来看看各自的走向。

 汇编实验结果如下

 通过观察结果我们发现,ptr1调用Func1是直接调用,而ptr2调用Func1的过程确实如此艰难尤其是中间还有个sub 8,这是为什么呢?

其实是调用Func1最终要靠*this指针来调用,注意*this指针指向的是Son类,ptr1之所以可以直接调用,是因为他切片出来的ptr1指向的地方和*this是相同的,所以可以直接调用Func1,而ptr2则不行,ptr2指向的是Base2的位置,与*this所指向的位置没有重合,会对其进行封装,将ptr2所指向的位置调整到*this指向的位置,再调用Func1,也就是中间有一个-8的指令起的就是这个作用

 总结

上面就是多态原理部分的所有内容,上面的实验均为博主完成,希望铁子们能够有所收获

猜你喜欢

转载自blog.csdn.net/zcxmjw/article/details/129978980
今日推荐