多态(虚函数)

多态分为静态多态和动态多态,一般我们说的多态都是动态多态
静态多态:程序在编译阶段就确定好地址。(函数重载、运算符重载、复用函数名)
动态多态:程序在运行阶段才会确定地址。(派生类和虚函数实现运行时多态)

(下面说的多态默认是动态多态)
多态满足的条件:
①有继承关系
②子类重写父类中的虚函数(重写是指函数的返回值类型函数名参数列表完全一致)
多态的使用:
③父类指针或引用指向子类对象

一个基类,如果只有一个普通的成员函数,那么sizeof这个基类的大小是1,因为需要有一个char类型的地址指向该基类(假装是一个实例化对象,因为也可以通过类名去访问类里面的数据)的地址。那么假如这个基类的成员函数加上了virtual修饰,那么这个成员函数就变成了虚函数(如果直接 = 0 就变成了纯虚函数,纯虚函数不能实例化对象,子类必须重写虚函数)。此时,sizeof这个基类的大小就变成了4,这个4是这个vfptr指针(虚函数指针)的大小。虚函数指针指向一个虚函数表,虚函数表里面存放着虚函数的地址。

此时如果子类继承了基类,那么在微观上看,在继承的一瞬间,是会把基类的这个虚函数表指针跟虚函数表一同继承下去,也就是说子类的vfptr一开始也是指向基类的虚函数表,但是在子类对象创建的时候,通过构造函数会把子类的vfptr指向自身的虚函数表。此时子类的虚函数表里面存放着的还是基类的虚函数的地址。但是只要子类重写了这个虚函数之后,那么子类自身的虚函数表里面记录的就是自身的虚函数地址(把基类的覆盖掉)。

此时如果使用基类类型的指针或者引用,指向子类的对象。那么调用该基类的虚函数就会执行已经被子类重写了的虚函数。
(这里要注意,子类的内存空间一般是要大于父类的,所以父类类型的指针指向子类的话,在创建这个指针的时候就把自己的内存变成跟子类一样大,所以不管怎么转换,都是安全的。)
在没有发生多态的情况下,基类转派生类是不安全的,派生类转基类是安全的。而如果发生了多态的话,怎样都是安全的。

如果子类中有开辟到堆区的内存,那么将发生内存泄露。因为delete父类类型的指针他只会调用自身的析构,也就是只会调用基类的析构函数。没有办法管子类。
所以引入了纯虚析构和虚析构函数,因为要防止父类里面也会发生开辟到堆区的内存,所以不管是纯虚函数还是虚析构函数,都需要有实现。如果是纯虚析构,要在类内声明,类外做实现(跟静态成员变量同理,原因呢?2021-12-6补充:原因是只要是父类指针指向子类对象,是一定最后会执行父类的析构函数的,所以基类一定要进行析构函数的实现,又因为语法规定了要在类内声明 = 0;所以只能在类外进行实现)。

当基类的析构函数被搞成了虚析构之后,delete基类类型指针的时候会先走子类的析构函数,为什么呢?因为子类里面的那个虚函数表呀!刚刚说的调用父类的引用或指针指向子类对象时,会实现子类重写了的虚函数,其实执行的时候内部会去子类的虚函数表里面找该函数的地址。同理,如果父类中virtual了析构函数,那么子类继承下来的时候,在子类的虚函数表里面也会有一个虚析构函数,虚机构函数和普通的虚函数一样,也可以覆盖。(这里会不会有个疑问,是怎么覆盖的呢?基类和子类的析构函数名字不同哦?实则上这部分编译器帮我们处理了,在一个虚函数表内只能有一个虚析构函数,所以虚析构函数的名字不同也可以覆盖)。

在实际开发中,一旦我们自己定义了析构函数,就是希望在对象销毁时用它来进行清理工作,比如释放内存、关闭文件等,如果这个类又是一个派生类,那么我们就必须将基类的析构函数声明为虚函数,否则就有内存泄露的风险。也就是说,大部分情况下都应该将基类的析构函数声明为虚函数。

再来补充一下什么时候需要用到虚函数:
①首先看成员函数所在的类是否会作为基类。
②然后看成员函数在类的继承后有无可能被更改功能,如果希望更改其功能的,一般应该将它声明为虚函数。
③如果成员函数在类被继承后功能不需修改,或派生类用不到该函数,则不要把它声明为虚函数。

在实际开发中,你可以定义一个抽象基类,只完成部分功能,未完成的功能交给派生类去实现(谁派生谁实现)。这部分未完成的功能,往往是基类不需要的,或者在基类中无法实现的。虽然抽象基类没有完成,但是却强制要求派生类完成,这就是抽象基类的“霸王条款”。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值