多态的概念
通俗的说多态就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的概念。
什么是多态
静态的多态
静态的多态即函数重载,编译时是参数匹配和函数名修饰规则。
动态的多态
运行时实现,跟指向对象有关。
父子类:
虚函数即被virtual修饰的类成员函数。
虚函数的重写:派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),也就是派生类的虚函数重写了基类的虚函数。
在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然可以构成重写,因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性,但是这种写法不是很规范,不建议使用。
虚函数重写的两个例外:①协变,即派生类重写基类虚函数时,与基类虚函数的返回值类型不同;基类虚函数返回基类对象的指针或引用,派生类虚函数返回派生类对象的指针或引用成为协变。②析构函数的重写,如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。看起来违背了重写规则,其实不然,可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。
析构函数建议是虚函数
为什么析构函数建议是虚函数?
A* ptr=new B;
delete ptr;
当通过基类指针或引用来操作派生类对象时,如果析构函数不是虚函数,那么在删除基类指针时,只会调用基类的析构函数,而不会自动调用派生类的析构函数。这将导致派生类的部分资源无法正确释放,可能会引发内存泄漏或其他资源泄露问题。
如果析构函数是虚函数,那么在删除一个指向派生类对象的基类指针时,将会根据对象的实际类型调用相应的析构函数,确保正确地清理资源。
如果析构函数不是虚函数,那么在删除一个派生类对象时,可能会导致派生类中特有的资源(如动态分配的内存、打开的文件句柄等)没有被正确释放,从而引发资源泄漏。
可以提高代码的健壮性和可维护性。
总的来说,如果一个类设计为可以被继承,通常建议将析构函数声明为虚函数,以确保在多态的情况下能够正确地释放资源和执行清理操作。
纯虚函数
在虚函数的后面写上=0,则这个函数为纯虚函数。
抽象类
包含纯虚函数的类叫做抽象类,也叫接口类。
抽象类不能实例化出对象,派生类继承后也不能实例化出对象,只有重写虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。
虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。
所以如果不实现多态,不要把函数定义成虚函数。
重载、覆盖(重写)、隐藏(重定义)的对比
override和final
C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,但是这种错误在编译期间是不会报出的,只有在程序运行时,没有得到预期结果的情况下由程序员debug才能得到修复。基于此,C++11提供了override和final两个关键字,来帮助用户检测是否构成重写。
final
修饰虚函数,表示该虚函数不能再被重写。
override
检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
多态的原理
虚函数表
一个类中如果有虚函数存在,那么在对象中,会存在一个指针,叫做虚函数表指针。一个含有虚函数的类中都至少有一个虚函数表指针。因为虚函数的地址要被放到虚函数表中。虚函数表也叫虚表。
虚函数表不仅存在于含有虚函数的对象中,同时也存在于它的派生类对象中。
如果派生类对虚函数重写,那么基类对象虚表和派生类对象虚表是不一样的。派生类对象存在的虚表是重写的虚函数,覆盖就是指虚表中虚函数的覆盖。重写是语法层面的说法,覆盖是原理层面的说法。
只有虚函数才会在被继承后放入虚表。
虚函数表本质是一个存虚函数指针的指针数组,一般情况下这个数组最后面放了一个nullptr。
派生类虚表生成过程:先将基类中的虚表内容拷贝一份到派生类虚表中;如果派生类重写了基类中某个虚函数,那就用派生类自己的虚函数覆盖虚表中基类的虚函数;派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
虚函数存在哪?虚表存在哪?应注意虚表存的是虚函数指针,而不是虚函数。虚函数和普通的函数一样,都是存在代码段的,只是它的指针存到了虚表中。对象中存的不是虚表。而是虚表指针。在VS下,虚表是存在于代码段的。
经典题目
参见资料。