五、虚函数和覆盖
1、虚函数
当成员函数前加 virtual 修饰后,这样的函数称为虚函数
该类也会像虚继承一样多了一个虚指针
2、虚函数表
虚指针指向一张表格的首地址,该表格中记录的是该类中所有虚函数的首地址
((void(*)(void))(*(int*)*(int*)b))();//相当于调用了虚函数表中的第一个函数
3、覆盖
当使用 virtual 关键字修饰父类的成员函数,此时父类中多了一个虚指针(虚表指针),子类会把父类的虚指针一起继承过来,当
子类中有与父类虚函数同名的成员函数时,编译器会比较这两个函数的格式,如果格式完全相同,则会把子类的同名函数的地址
覆盖掉虚函数表中父类的同名虚函数的原地址
此时使用父类的指针或引用指向子类对象时,调用虚函数则会调用被覆盖后的虚函数表中所指向的子类的同名且格式相同的
成员函数
4、构成覆盖的条件
1、必须是发生在父子类中,且一定为public继承
2、要被覆盖的父类的函数必须为虚函数 virtual 修饰
3、子类中必须有与父类虚函数同名,且返回值、参数列表、常属性都必须完全相同的函数,才能构成覆盖
4、覆盖要求返回值类型相同,或者子类函数的返回值可以向父类虚函数返回值类型做隐式转换且有继承关系时,也可以构成
覆盖
常见面试题:重载、覆盖、隐藏、重写的区别?
隐藏:
1、如果同名但格式不同,无论是否加 virtual ,在子类中都会隐藏父亲同名成员函数
2、子类中如果同名且格式相同,不加 virtual ,子类也会隐藏父类的同名成员函数
总结:父子类中,同名成员函数要么构成覆盖,要么构成隐藏
七、多态
什么是多态:
是指同一个事物、指令可以有多种形态,当调用同一个指令时,它会根据参数、环境的不同会做出不同的相应操作,这种模式
称为多态
C++中根据确定执行操作的时间,多态分为编译时多态、运行时多态
编译时多态:
当调用重载过的函数时,编译器会根据参数的不同,在编译时就能确定执行哪个版本的重载函数,这种叫做编译时多态,
还有模板技术
运行时多态:
在父子类中,当子类覆盖了父类的同名虚函数,然后用父类指针或引用访问虚函数时,它既可能调用父类的虚函数,也可能
调用子类的同名函数,具体调用哪个取决于该父类指针或引用指向的目标是谁,而这个目标的确定需要在运行时才能最终
确定下来,这种情况叫做运行时多态
构成运行时多态的条件:
1、父子类之间且有同名函数
2、子类是以public继承父类(让父类指针、引用指向子类对象)
3、通过父类指针或引用访问被覆盖的虚函数
思考:构造函数和析构函数能否是虚函数?为什么?
八、虚析构和虚构造
虚构造:
构造函数不能设置为虚函数,假如构造函数可以设置为虚函数,子类的构造函数会自动覆盖父类的构造函数,当创建子类对象时,
子类执行自己的构造函数之前先执行父类的构造函数,但是此时父类的构造函数已经被覆盖成了子类的构造函数,就会再次调用
子类的构造函数,这样就形成了死循环,因此编译器不允许把构造函数声明为虚函数
虚析构:
析构函数可以设置为虚函数,当使用类多态时,通过父类指针或引用释放子类对象时,默认情况下不加虚析构是不会调用子类的
析构函数,如果子类析构函数中有要释放的资源时,就可能构成内存泄漏
只有把父类的析构函数定义为虚析构(子类的析构函数会自动覆盖父类的析构函数),当通过父类指针或引用释放子类对象时,
会先调用覆盖后的子类析构函数,而且之后还会自动调用父类的析构函数,这样就不会有内存泄漏了
总结:
当使用多态且子类的构造函数中有申请内存,此时父类的析构函数一定要设置为虚析构
九、纯虚函数和纯抽象类
纯虚函数的格式:
virtual 返回值 成员函数名(参数列表) = 0;
1、纯虚函数可以只声明不实现(一般也没必要去实现它)
2、父类中如果有纯虚函数,子类就必须覆盖父亲的纯虚函数,否则无法创建对象
3、有纯虚函数的类是无法创建对象的,因为这样的话纯虚函数没有被覆盖
4、纯虚函数的目的就是为了强制子类去覆盖父类的纯虚函数,强制子类实现某些功能
5、有纯虚函数的类称为抽象类
6、析构函数可以定义为纯虚函数,但是需要在类外去实现它
纯抽象类:
所有的成员函数都是纯虚函数的类,叫做纯抽象类,这种类一般用于设置功能的接口,也称为接口类