多态前言:
什么是多态?
多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
不同的对象执行同一段代码会调用不同的函数
目录
- 如何才能实现多态
- 什么是重写?
- 重写、重载、重定义对比
- 多态的运行原理
- 虚表深入探索
- 单继承虚表
- 多继承虚表
如何才能实现多态
- 派生类必须对基类的函数进行重写
- 必须通过基类的指针或者引用来调用函数
- 被调用的函数必须是虚函数
什么是重写?
重写:两个函数的参数列表,函数名,返回值必须全部相同
重写的两个例外:
- 派生类的重写虚函数可以不加virtual(建议大家自己写的时候加上)
- 协变:返回的值可以不同,但要求返回值必须是父子关系的指针或引用(不常用)
举例1:
举例2:
重写、重载、重定义对比
多态的运行原理
我们来算下Person和Student的大小:
不是我们预期的4和8,为什么呢?
在Person对象里有一个虚函数表(指针数组),指向代码段的函数地址
注意 :派生类对象自己不会单独产生虚函数表,派生类的表是继承基类的。
为什么必须是父类的指针或引用,而不是子类的?
切片可使父类的指针既指向子类对象又可以指向父类对象
为什么父类的对象不能实现多态,只能父类的指针或引用实现?
因为子类赋值给父类对象的时候,父类并不会拷贝子类的虚函数表
多态运行原理:
- 按编译规则查找函数,找到了在看该函数是否构成重载或多态
- 若构成多态,则在运行时才会取函数地址,运行时通过指针找出对象, 并通过对象的虚函数表中找到要调用和函数地址
虚表深入探索
虚表在内存的哪个位置?
虚表在vs下的存储是代码段里
如何验证??
可以通过对地址的分析来确定,下图中验证了虚表里常量区的更近,所以在vs下可以看出,虚表存储在常量区
单继承虚表
子类的虚表由以下三部分构成
- 会拷贝父类虚表
- 子类重写的函数会覆盖父类函数的地址
- 再添加自己的虚函数
由于编译器监视窗口的优化,我们不能直接看监视窗口里虚表的内容。则需要我们自己打印虚表
如何打印虚表
思路:取出b、d对象的头4bytes,就是虚表的指针,前面我们说了虚函数表本质是一个存虚函数指针的指针数组,
这个数组最后面放了一个nullptr
1.先取b的地址,强转成一个int的指针
2.再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针
3.再强转成VFPTR,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。
4.虚表指针传递给PrintVTable进行打印虚表
5.需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最后面没有放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的-生成-清理解决方案,再编译就好了。
多继承虚表
为什么这里Derive虚函数表里两个func1()的函数指针不同?
你们发没发现基类指针调用重写函数,传的是基类的this指针,而在重写函数里确可以无差错的调用派生类里的成员
如:
明明B*的this在_a成员的下面却可以调用_a成员。
实际上基类指针调用重写函数,虽然传的是基类的this指针,最后编译器会将this指针处理成派生类的指针,使之能正确的访问到派生类里的每个成员。
最后表现出了Derive虚函数表里两个func1()的函数指针不同
若要追究编译器如何将基类的this指针处理成派生类的this指针,大家可以去看看汇编,简单来说就是call时对地址的封装,让基类的this指针减去第一个继承类的大小,this就指向了派生类的首地址,就可以当作派生类的this地址了