我们知道类的3大特性是封装,继承,多态。前2个将差不多了,多态其实也不难。
多态的基本概念
多态分为静态多态和动态多态
函数重载和运算符重载属于静态多态,复用函数名
动态多态是派生类和虚函数实现运行时多态
区别:
静态多态编译时绑定函数地址,而动态多态是运行时确认函数地址(动态比静态多)
1我们为了理解动态多态,先写一个最简单的,动物有叫的方法,猫继承,重写了方法,注意是重写,不是重载,比重载要求严格是返回值也必须一样。我们编写Animal的引用给speak方法,里面调用shout方法,我们在测试方法里将Cat对象传入,注意C++里是允许将父类引用或者指针指向子类的,这时我们传入了猫,你会以为是猫叫,结果却是动物叫,是类型限制了么,我传入了cat对象。其实这就是静态多态,已经预先定好了内存的位置。所以不管传什么子类都是调用的动物类方法。
2我们可以把继承变成静态继承,就是将父类方法变成虚函数,而虚函数就是函数前加上virtual,子类cat继承后方法可以不写virtual,写上也没错,如图,就变成运行时调用,这样就实现了猫在叫。
总结,动态多态得有继承关系,子类要重写父类的虚函数,子类的virtual可以不写。动态多态的使用是父类的引用或指针指向子类的对象
3当我们给Animal定义虚函数时,如上图,其内部就会有个vfptr虚函数指针,指针指向其vftable虚函数表,函数表内记录的是虚函数的地址。
4如果Cat只是继承,没有重写,则会把虚函数指针继承,同时把父类的虚函数表继承,里面的是动物的函数地址。
5当Cat重写后,虚函数表就替换成了自己的方法的地址。当我们使用多态方式调用,即父类指针指向时,还是去依据指针指向虚函数表,但是指向变了,这就是多态的原理
6当然还是可以通过vs命令行去看结构。
多态案例
为什么使用多态,组织结构清晰,可读性强,便于前期和后期的扩展以及维护,尽可能满足对修改关闭对扩展开放的原则
7如上,我们一般如果自己写一个计算器类,可能会考虑将每个计算方法都添加到类里,但是哪天再添加就需要修改他,而如果我们只定义一个基类,对计算结果留一个接口,然后使用各种类去实现它,然后就可以实现多态的调用,我们这里使用的多态的指针的引用。因为是new出来的空间,所以需要我们手动去释放它,结束要delete掉
纯虚函数和抽象类
我们之前多态虚函数,也写了动物类的shout函数内容,计算器类也写了返回0,其实很多时候我们并不需要这个父类去实例化,所以父类的虚函数定义什么内容就无意义,这时候我们就需要使用什么都没有内容的纯虚函数,而使用了纯虚函数的类叫做抽象类,抽象类是不允许实例化的,也不允许在堆区占用空间,而子类如果不多态重写纯虚函数,则其也是抽象类,直到重写了虚函数为止。
定义纯虚函数的格式 virtual 函数名(参数列表)=0; 这里并不需要函数内容。只等着子类去实现注意有=0及分号
8如上,我们尝试去实例化一个抽象类,是不可以的
9如上,我们子类没有重写虚函数,也是抽象类,同样不让实例
10当我们重写了抽象类的方法,就可以实例化,也能调用方法,因为这个方法有意义
虚析构和纯虚析构
11首先先来个上面的代码,我们猫继承动物类,分别写出构造和析构打印,将子类继成抽象类,同时使用多态写法指针接收对象,用完需要删除指针,可以看到多态写法并没有执行Cat的析构。如果我们给Cat开辟堆区的属性,那就无法被释放掉,这时我们就可以使用虚析构,就是在父类的析构方法加上virtual关键字,就会实现调用cat析构
12如上,我们给cat定义了堆区指向的指针,因为我们要析构时释放,所以需要加上经常写的释放语句,但是这里需要给父类的析构加上virtual关键字,这样子类的析构就会在多态的情况下也会被执行。
纯虚析构当然你会简单的以为就是virtual ~父类名()=0;但是如果这么写是不行的,因为父类的析构不被实例化,不能被调用,如果父类开辟了堆区空间呢,就还是希望被调用方法,这时就需要在类外作用域重载虚析构方法。但其实这么写父类本身也就无法实例化了,变成了抽象类。
13
网友评论