一.对多态的解释
场景:买车票时,学生是半价,军人要优先......,对于不同的人群,在同一个售票窗口会受到不同折扣,这就是多态的体现。
上图就是多态的效果。
为什么Ticket的参数是person类型,但却能接受不同类型对象并打印出不同的信息呢?
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
二.多态的实现
我们知道student和solider的父类都是person,多态的实现就是依靠对父类函数的重写(也叫覆盖)。看一下上面引子是如何实现的:
步骤:
1.在父类中找到想要呈现出多态的函数,用virtual修饰。
2.在子类中重写这个函数,要求函数名,返回值,参数必须相同,子类中的virtual可加可不加,但是父类中的virtual一定要加,表示这个函数将被重写(这个函数也叫虚函数)。
3.在不同对象传入函数(售票口)时,必须用父类的引用或者指针接收!!
补充:多态条件下(一定要注意),在子类中重写函数后,重写的是函数的实现,所以说你可以看成重写后的函数的函数名,参数,返回值都用的是父类的:
以上程序构成了多态,但打印的是1不是0,原因就在这儿。
如果重写了却没有构成多态,比如直接用s调用func,打印的就是0。
---------------------------------------------------------------------------------------------------------------------------------
为什么要用父类的引用和指针接收呢?(以下是我的胡乱理解,不一定准确,但可以暂时帮助记忆)
student,solider和 person(父类)不是同一个类型,根据继承的那篇文章所说,把student和solider对象作为参数传入Ticket,而Ticket的形参是person,这样就会有赋值转换发生。
如果是引用和指针的话,指向的是实参student和solider中person成员所处的空间
这样引用和指针就有机会找到没有转换成person之前student和solider重写的同名函数,所以虽然在Ticket中对象类型都是person,但是指向的分别是student和solider的空间,调用的自然可以是不同对象的同名函数。
而直接赋值会导致直接把student和solider中属于person的成员拷贝到另一个空间,这样就算有virtual修饰也没法找到两种不同的同名函数,只会调用person原有的函数。
---------------------------------------------------------------------------------------------------------------------------------
注意多态的两大条件:
重写虚函数
用父类指针或者引用调用虚函数(用对象直接调用叫隐藏,即子类重写函数把父类的函数给隐藏了,这时候就和多态没什么关系了)
---------------------------------------------------------------------------------------------------------------------------------
至于为什么不用对象直接调用各自对应的重写了的函数,比如这样:
子类函数把父类同名函数隐藏,所以优先调用子类重写的函数。
我现在不清楚。
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
三.虚函数重写的两个例外
1. 协变(基类与派生类虚函数返回值类型不同) 。派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数的返回值和派生类虚函数的返回值是父子关系并且基函数返回父类对象的指 针或者引用,派生类虚函数返回子类对象的指 针或者引用(了解)
2.析构函数的重写(基类与派生类析构函数的名字不同)。
创建了一个student对象,并且用person类的指针指向这个student对象,直接delete p的话只会调用person类的析构函数,这样会导致student中开辟的动态空间不被释放,俗称内存泄漏。
为了达到这个目的:p指向student就调用student的析构,p指向person就调用solider的析构
那么就要用多态,让析构函数成为虚函数,但是父类和子类的析构函数不是同名函数,不满足形成多态条件。为了帮助我们完成这个目的,编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor,这样就符合多态条件了。
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
四.C++11 中的 override 和 final
1. final:
在父类中修饰虚函数,表示该虚函数不能再被重写:
final也可以修饰整个类,表示该类不能被继承:
2. override:
检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
五.抽象类(接口类)
在虚函数的后面写上 = 0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口 类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写了所有纯虚函数之后,派生类才能实例化出对象。
为什么会有抽象类:想象一下基类是人类,派生出中国人,小日子,英国佬等子类,说这个人是中国人很合理,但是总不能说这个人是人类吧,也就是说,我们不需要用父类创建对象的时候把它定义为抽象类。
------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
六.多态的原理
---------------------------------------------------------------------------------------------------------------------------------
我现在解释一下,只要类中含有虚函数,包括继承父类的,类所生成的对象就会储存一个指针,这个指针指向一个函数指针数组(也叫虚函数表),这个函数指针数组里存放着函数的地址(如果虚函数在子类中被重写,存放的是重写的函数地址,如果没重写,就存放的是父类虚函数的地址)
---------------------------------------------------------------------------------------------------------------------------------
从这个角度理解一下多态如何产生:
a传入函数后,x指向的是a中属于父类的一部分,但是子类继承父类后,由于sound这个虚函数被重写,其指针p所指向的指针数组里的sound的地址不是父类的sound而是子类重写的sound1。
而在调用函数时,如果满足多态,就会从这个指针数组里去找函数地址去调用,找到的是sound1,所以调用的是子类的sound1而不是父类的sound。
相反,由于在子类Cat中没有重写face函数,其指针p所指向的指针数组的里的face函数的地址没有改变,仍然调用的是父类的函数。
---------------------------------------------------------------------------------------------------------------------------------
强调几点:
1.只要是虚函数,都会进入虚函数表,不管重写没重写。
还有就是我上文提到一个函数在父类中加了virtual,在子类中重写时可以不用加,就算不加,它也是虚函数,也会入虚函数表。
2.子类继承父类后,虚函数表会因为重写而改变。其实多态为什么能实现对象不同调用的函数就不同,虽然都是用父类指针或引用接受对象(这是多态的形成条件),但是因为空间还是原对象的空间,虚函数表也是原对象类型的。虚函数表里的函数地址与父类不同,调用的函数自然就不同
这也侧面反映了为什么要用引用或者指针接收而不用赋值。
3. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态, 比如:函数重载
4. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体 行为,调用具体的函数,也称为动态多态。类的多态就是这样,编译器一但确认这个函数调用符合多态,就暂时不会确定调用哪个函数,而是在运行时访问p指针取出想要的函数指针,根据指向的对象类型去调用虚函数表里的函数。