函数名前加virtual是虚函数
子类和父类的虚函数如果函数名,参数和返回值都相同,那么子类的虚函数就和父类的虚函数构成重写(也叫覆盖)的关系。
而如果把virtual都去掉,那么他俩就是隐藏关系。
多态条件:
1.虚函数重写是多态条件之一
2.父类的指针或引用调用虚函数(注意父类对象不行)
两个条件都需要满足
红线部分都可以调用。子类调用相当于是父类切片,但也是子类对象做这个事情。
切片是让父类的指针或引用指向子类,但是它看到的是子类中父类的部分。
此时如果进行如下修改,那么结果如下图所示。
如果再加上这个
那么上图的结果是
如果不是引用去调用虚函数
那么军人和学生调用就相当于切片,就相当于普通调用。
如果用指针调用,也符合多态。
子类虚函数可以不加virtual它是把父类的虚函数继承下来,但重写了虚函数的实现。
协变:派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指 针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。
这样也可以
因为s是后创建的,所以s先析构,此时s调用了一次自己的析构,又调用了继承下来的Person中的析构,然后P是先创建的,第二次析构是P自己调用的自己的析构
如果把~person变成虚函数
上面的调用结果如下,不受影响。
- 那么析构函数是否建议定义为虚函数呢?
现在把virtual去掉,下面这个代码没有调用出Student的析构函数。会发生内存泄漏。
因为它是普通调用,ptr2是Person类型的,所以会调用Person的析构。
那如果想要多态调用呢?
因为子类析构函数和父类析构函数构成隐藏关系(由于多态关系需求,所有析构函数都会特殊处理成destructor函数名),底层会对析构函数名进行处理,子类和父类的析构函数在底层其实是同名的,在不同的环境下 构成隐藏,重写等关系。
必须让它强行相同,才能符合多态条件。
父类和子类析构函数因为被修改了,它俩这样写在执行过程中名字被视为相同,构成三同。
实现父类时,可以无脑地给析构函数加virtual。
- 如何实现一个不能被继承的类
- 父类构造函数设为私有。继承下来的成员必须调用父类构造函数初始化,但私有后父类构造函数子类不可见,就不能成功继承。
而如果把析构函数私有
这样不能成功继承。
但这样就能成功继承了。
2.类定义时加final。
final修饰虚函数表示虚函数不能被重写。
override放在子类检查(编译时检查)是否完成重写。
把划线部分原先的virtual去掉,就会报错
参数不同也会报错。
- 抽象类
包含纯虚函数的类叫抽象类。
抽象类不能实例化出对象。
BMW不能实例化出对象,纯虚函数被继承下来了,BMW也是抽象类。
而如果子类重写的话,BMW就能成功实例化出对象。因为重写后BMW中的就不是纯虚函数了。
虚函数的重写是接口继承,普通函数继承是实现继承。
子类也可以不加virtual。
抽象类一定程度上起到了让子类虚函数重写的作用。
不需要类的对象,可以用抽象类。
func构成虚函数重写,P调用test函数,实际是A调用的,test函数中的func由this指针调用,这个this指针是A*,所以这个func属于多态调用,(test被继承下来了,但是this指针仍然是A*)由于p是B*,所以它调用的是B中的func,但是重写时,子类接口用的父类,不加virtual也是虚函数,所以它缺省值用的父类,所以打印的是1。
而如果下面这样写。
结果如下
因为调用func的this指针是B*,不满足父类指针或引用调用虚函数的条件,所以是普通调用,func就不会虚函数重写,(多态调用用的是虚函数的重写),调用的是B中func本身。
选C, 切片时指针会偏移 ,p1和p3相等,但p1看的是切片Base1部分,p3看的是整体。
这样是4个字节
这个大小是12个字节,里面有个指向虚函数表的指针(vftptr)在32位系统下占4个字节。
虚函数表本质是函数指针数组。里面存的是虚函数的指针。
完成重写的虚函数,虚函数表的对应位置覆盖成重写的虚函数。
父类和子类虚函数表不同。
- 多态的原理
调用时,虚函数指针指向父类虚函数表,那么就调用父类的虚函数,指向子类的虚函数表,你们就调用子类的虚函数。
静态指的是编译时。比如函数重载。
- 虚函数表存放在哪个位置?
be对象指针十分接近常量区,虚函数表就是存放在常量区的。
底下这两个是一回事。
同一个类型共享一个虚表,be,b1和b2虚表是同一个。
我们发现子类续表中没有fun3,实际是监视窗口进行了优化。
在内存监视窗口其实是有的
函数指针typedef定义的变量位置必须放中间。
函数指针加括号就能调用函数了
不是重写的虚函数放在了虚表的后面。
这样也可以
- 多继承的虚函数
现在只打印了一张虚表。
这样可以打印第二个虚表
多继承中,自己的虚函数(func3)放到第一个继承的类中的虚函数表中。
这种方式也能打印第二个虚表。
多继承不一定有多个虚表。如果有一个类中没有虚函数,你们就会少一个虚表。
初始化列表初始化的顺序和写的顺序没关系,是按声明顺序初始化的,在继承里面就是学先继承,谁先被初始化。
调换顺序结果也是一样的。
初始化列表是要对父类进行初始化,如果B和C是直接继承,把virtual去掉,那么D是菱形继承,不是菱形虚拟继承,此时D中只有B和C,初始化列表就没有A了。
下图结果p1>p2=p3
- 内联函数能是虚函数吗?
多态调用时编译能通过,但编译器会忽略内联属性,它要运行后到虚函数表中去找。
多态调用忽略内联属性,普通调用继续保持内联属性。inline是建议,但是编译器不同意这个建议。
父类的对象为什么不能调用多态。
如果父类对象被子类切片赋值过,那么父类的虚表就是子类的虚表,会产生矛盾。
- 静态成员函数不能是虚函数。
静态成员函数没有this指针,变成虚函数没有意义。
- 构造函数能不能是虚函数
不能,虚表指针是在初始化列表初始化的,而且没有意义。
- 析构函数最好设为虚函数。
- 访问普通函数快还是虚函数快?
如果是普通调用,那就是一样快,如果多态调用,那么是普通函数快。
- 虚表是编译时生成。
构造函数初始化列表中初始化虚表指针。
虚函数表存的是虚函数的地址,为了实现多态,虚基表存的是偏移量,为了找到虚基类。
抽象类强制重写虚函数,抽象类体现了接口继承关系。