多态基础第二部分
二多态原理
引入(多态原理)
计算下面虚类的大小:
1 | class Base{ |
如果是一般的类,那我们会认为是计算结构体对齐之后的大小,结果应当是 8。
但计算结果发现,虚类的结果是 12 ,说明虚类比普通类多了一些东西.
实例化对象Base b; 可以发现对象的头部多了一个指针 _vfptr; 这个指针叫做虚函数表指针,它指向了虚函数表
虚函数表指针
指向虚表的指针,叫虚函数表指针,位于对象的头部.
定义:
如果在类中定义了虚函数,则对象中会增加一个隐藏的指针,叫虚函数表指针__vfptr,虚函数表指针在成员的前面,直接占了 4/8 字节.
虚函数表/虚表
虚函数表指针所指向的表,叫做虚函数表,也叫虚表
虚函数表本质是一个虚函数指针数组,元素顺序取决于虚函数的声明顺序。大小有虚函数的数量决定。
虚表的特性(单继承)
- 虚表在编译期间生成.
虚表是由虚函数的地址组成,而编译期间虚函数的地址已经存在,因此能够在编译期间完成.
- 虚函数继承体系中,虚基类先生成一份虚表,之后派生类自己的虚表都是基于从父类继承下来的虚表.
特例,为了方便使用, VS 在虚表数组最后面放了一个 nullptr.(其他编译器不一定有)
- 子类会继承父类的虚函数表(开辟一个新的数组,浅拷贝)
- 如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数,如果子类没有重写,则虚函数表和父类的虚函数表的元素完全一样
- 派生类自己新增加的虚函数,从继承的虚表的最后一个元素开始,按其在派生类中的声明次序增加到派生类虚表的最后。
- 派生类自己新增的虚函数放在继承的虚表的后面,如果是基类则是按顺序从头开始放,总而言之,自己新增的虚函数位置一定比继承的虚函数位置后
- 虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中.另外对象中存的不是虚表,存的是虚表指针
- 虚表是在编译阶段就完成了,在初始化列表完成的是虚表指针的初始化
- 同一类型直接定义的对象共享同一个虚表
- 子类对象直接赋值给父类对象后就变成了父类对象,只拷贝成员,不拷贝虚表,虚表还是父类的
注:虚表指针和成员谁先初始化由编译器决定
虚表的位置
虚表没有明确说必须在哪里,不过我们可以尝试对比各个区的地址,看虚表的大致位置
根据地址分析,虚表指针与常量区对象地址距离最近,因此可以推测虚表位于常量区.
另外,在监视窗口中观察虚表指针与虚函数地址也可以发现,虚表指针与虚函数地址也是比较接近,也可以大致推测在代码段中.(代码段常量区很贴近,比较 ambiguous ,模棱两可的)
从应用角度来说,虚表也应当位于常量区中,因为虚表在编译期间确定好后,不会再发生改变,在常量区也是比较合适的.
多继承虚表
先看虚函数多继承体系下内存布局
1 | class Base1 { |
简单分析可知,虚函数多继承体系下派生类会根据声明顺序依次继承父类.继承方式类似于虚继承.
那么多继承下子类自己新增的虚函数在哪?
我们知道,单继承中,子类自己新增的虚函数会尾插到虚表的末尾.
那么多继承呢?是每个父类都添加?还是只添加到其中一个?添加到一个的话添加到哪里?
通过结果能证明,子类自己新增的虚函数只会添加进第一个继承的父类的虚表中,也就是尾插.
子类会继承所有父类的虚表,有多少个父类就有多少个虚表结果也证明,子类重写会对所有父类的同名函数进行覆盖
观察结果还发现,两个 func1 的地址居然不一样.这其实涉及到 C++ this 指针的原理问题 -> this 指针修正.
虚表中地址(概念修正)
虚函数地址:这是虚函数在程序内存中的实际地址,即函数体开始的位置。
虚表中的地址:虚表中存储的地址通常直接指向虚函数的实际地址。然而,在某些情况下,如为了实现一些优化,编译器可能不会直接在虚表中存储虚函数的地址,而是存储一个“跳跃”函数的地址,这个跳跃函数再跳转到虚函数的真实地址。这种跳跃函数可以用来做额外的检查或者优化,例如性能计数、调试信息插入等。
菱形继承+多态 与 菱形虚拟继承+多态
菱形继承的问题:菱形继承主要有数据冗余和二义性的问题。由于最底层的派生类继承了两个基类,同时这两个基类有继承的是一个基类,故而会造成最顶部基类的两次调用,会造成数据冗余及二义性问题。如下图所示,在Assistant的对象中Person成员会有两份。
什么是菱形虚拟继承?如何解决数据冗余和二义性的?
虚拟继承可以解决菱形继承的二义性和数据冗余的问题。如上面的继承关系,在Student和Teacher的继承Person时使用虚拟继承,即可解决问题。需要注意的是,虚拟继承不要在其他地方去使用。
虚拟继承解决数据冗余和二义性的原理
在虚拟继承中,编译器会确保在整个继承体系中只有一个基类实例。编译器会在派生类的对象布局中使用指针来指向基类部分,而不是直接嵌入基类部分。这样,最终派生类中的基类部分指针指向同一个共享的基类实例。
内联函数inline 和 虚函数virtual
inline如果被编译器识别成内联函数,则该函数是没有地址的. 与虚表中存放虚函数的地址有冲突.
但是事实上,inline 和 virtual 是可以一起使用的
- 这取决于使用该函数的场景:内联是一个建议性关键字,如果发生多态,则编译器会忽略内联.如果没有发生多态,才有可能成为内联函数,即:多态和内联可以一起使用,但同时只能有一个发生
静态函数static 与 虚函数
静态成员函数不能是虚函数,因为静态成员函数没有 this 指针,与多态发生条件矛盾
父类引用/指针去调用
static函数没有隐藏this参数.不满足虚函数重写条件”三同”
静态成员函数目的是给所有对象共享,不是为了实现多态
构造函数、拷贝构造函数、赋值运算符重载 与 虚函数
- 构造,拷贝构造不能是虚函数
1. 构造函数需要帮助父类完成初始化,必须一起完成,不能像多态那样非父即子(父对象调父的,子对象调子的);
2. 虚表指针初始化是在构造函数的初始化列表中完成的,要先执行完构造函数,才能有虚函数
3. 构造函数多态没有意义
- 赋值运算符重载也和拷贝构造一样,不建议写成虚函数,虽然编译器不报错.