多态
所谓的多态其实就是多种形态,它又被分为编译时多态(静态多态) 和 运行时多态(动态多态)。
静态的多态其实就是之前的模版和函数重载,今天我们主要讲动态的多态。所谓的动态多态其实就是相同的函数,完成不同的功能。
这就实现了明明都是用A&类型的参数,但是调用的结果不一样的情况出现。要达成这一点其实有两个地方需要注意,首先,必须是父类的指针或者引用,不然切片切不好。其次,需要父类和子类函数的返回类型、函数名、参数类型都要一样,这样,在前面加上一个virtual就可以达成虚函数的重写了。
当然这里有两个需要注意的地方:子类可以不加virtual,但是父类一定要加。另一个则是协变,这个比较少出现,它允许返回类型可以不一样,不过必须是派生类的引用或指针,也许是考虑到可能派生类里面就包含有父类的关系。
一旦上面的两个条件不满足,那就根据传入的是A,两次都调用666。所以在写多态之前先检查一下自己的条件满不满足。
同时它也不是非得局限于一个已有函数的另一种方法,它可以只是一个笼统的对象。
这里有一个老六题,做完这个,想必理解会大大提升。
根据我们之前讲的,毋庸置疑这是一个虚函数的重写,之后创建了一个B类型的指针,然后再调用了test,这里是第一个坑,大伙可能觉得,这B指针不满足两个条件啊,但实际上,因为继承的关系调用test其实是把B切片了传入到A里,所以test的this是A*,满足了条件,第二个坑来了,按照我们说的,各自实现各自的,那么这里应该会打印B->0。 但是实际上打印的是B->1.
为什么呢,因为这个是重写,我用的参数并不是用B的 我用的是A的,只有内容我才按B的实现。
所以我用A的缺省参数来实现B的打印,自然就变成了B->1这么个奇怪的答案。
所以咱以后注意虚函数的重写部分就好了,派生类的缺省值就别参考了。
析构函数其实也有坑,或者说隐藏点,那就是析构函数其实都会被处理为为destructor,所以virtual加析构函数也是达成重写的,那么为什么要这么做呢。
其实就是为了应对这种情况的,站在我们的角度当然知道p1调用A的析构 p2调用B的析构,但是编译器不知道,它只会根据类型调对应的析构,那这就出现内存泄漏的问题了,所以祖师爷采取了这种方法来解决问题,所以,在父子类之中有用到new的,顺便把析构给实现重写。 也因为会被处理为destructor所以如果即使没有new,那父类和子类的析构也是构成隐藏关系的。同时子类调用构造是先调用父类再调用子类,那么到了析构,就是反过来先调用子类,再调用父类。而子类必定是包含有父类的内容的,所以子类的析构还会额外调用一次父类的析构。
到这里想必一定有粗心的兄弟,可能不会时时刻刻关注虚函数是否满足两个必备条件,所以祖师爷给了我们一个关键字 override 它可以判断你是否构成重写,如果是,那就无事发生,不然就给你报错,可以理解为虚函数专属的assert 不过它不会温和的提示你出问题,而是直接无法运行。
它会很明确的告诉你没重写。
而如果我们不想有别人重写我们的父类,那我们可以用到另一个关键字final。可以翻译理解为最终类,无法被继承和重写。 放类名后就是最终类,不能被继承,放函数后面就是不能被重写。
不过这俩是后面更新出来的,也许有一些非常老的版本是不认的。
这些概念其实如果理解了,是不用专门记的。
抽象类
其实就是之前我们不写函数实现部分,只给参数返回类型。
这玩意其实就是只给你参数,但是不给你实现,所以它不能被实例化。但是父类的指针或者引用是可以的。实现交给子类来写,子类不写光继承也不能实例化出对象来。这样也代表了它没有父类对象。完全根据子类写的实现运行。
有点类似于模版。
虚表
咱笼统的理解起来就是 带virtual的它内部都有一个隐藏的指针,所以计算大小要多算一个指针进去,这个指针准确的说是函数指针数组,它是虚函数用来存储被重写的函数的地址的。
这个vfptr其实就是虚函数的指针,它的大小取决于你这有多少个虚函数。
我们创建多少个对象,就有多少个虚表。这个过程呢就是如果你满足多态,那么我不会和正常函数一样直接调用,而是去你的虚表里面找你的地址,调用的是地址里的函数,私人订制实现。
而父类子类之间,虚表表面上其实是一个虚表,至少刚切片完是这样的,但是如果你重写了实现,那么子类的虚表就会更新覆盖之前的函数地址,所以此时虽然它表面还是父类的虚表,但是实际上它的内容已经是子类重写后的了。这也是虚函数能实现重写的本质。
知道了这一点,是不是就感觉它没那么高级了,遇到虚函数的动作都是一样的,只是它里面的地址更不更新的区别,更新了就不一样,没更新就一样。
可以看到,虚函数它里边多了很多指针跳转的步骤,一层一层找进去,然后才call的地址。
而普通的函数就很短了,我才不管你是谁,我直接根据你的地址找进去然后call就完了。
这上面也是静态绑定和动态绑定的区别,静态就是普通函数调用,虽然也能调用,但是显得有点呆板,而动态就是虚函数的调用,它就很智能,至少看起来很智能。不过动态这么多行指令效率就会比静态低一点。
小小总结一下:
虚表不同只是不同类用不同的虚表,同类其实是共享一个虚表的,因为咱的函数实现是一样的,那何必浪费内存区创建多个冗余对象呢。
而子类由两个部分组成,我们之前提到的继承下来的父类虚表和自己的虚表。表面上父类还是父类,实际上里面存的是重写后的地址。
子类的虚表又由三个部分组成:父类的虚函数地址,子类的虚函数地址,重写后的虚函数地址。
所以虚函数的本质其实是一个存虚函数指针的指针数组,而这个数组后面其实放了一个0x00000000的标记,类似于\0的作用,不过这个不是C++规定的是各个编译器定义的,vs系列会放,g++就没放。
那么虚函数表存在哪里呢,堆区?有点不太合理,因为我们并没有看到它的释放。栈?出了作用域销毁显得更不合理了,经过对比一下发现这玩意存在常量区里,不过这是vs的版本,实际上c++没规定放哪,所以可能会因为编译器的问题有所差异。