概念
通俗来说,就是多种形态。多态分为编译时多态(静态多态)和运行时多态(动态多态),我们重点介绍运行时多态。编译时多态(静态多态)主要就是我们前面讲的函数重载和函数模板,他们传不同类型的参数就可以调用不同的函数,通过参数不同达到多种形态,之所以叫编译时多态,是因为他们实参传给形参的参数匹配是在编译时完成的,我们一般把编译时归为静态,运行时归为动态
运行时多态,具体点就是去完成某个行为(函数)(情况不同行为就会随之不同),传不同的对象就会完成不同的行为,就达到多种形态。比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是优惠买票;军人买票时是优先买票
具体来说,多态是一个继承关系的下的类对象去调用同一函数,产生了不同的行为
多态的实现条件:1、必须由基类的指针或引用来调用 ,因为只有基类的指针或引用才能既指向基类对象又指向派生类对象2、被调用的函数是虚函数(类成员函数前加关键字virtual即成虚函数,非成员函数不能加virtual),派生类必须对基类的虚函数完成重写/覆盖
派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同,但参数的默认值可以不同)就构成虚函数的重写/覆盖
在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了,在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用。不过在考试选择题中,经常会故意买这个坑,让你判断是否构成多态
这段代码的输出结果是什么
默认参数是在编译时静态绑定的,当编译到“func();”,此时处在类 A 的作用域,编译器看到的是 A 声明的“func(int val = 1)”,编译时期直接改写代码为“func(1)”。p是指向类B的一个对象的指针,test()函数被继承到派生类B,p->test()指调用test()函数,从前面类和对象的知识我们知道成员函数的参数列表隐含有当前类类型(基类A)的指针this,p传给this,"func(1);"其实是“this->func(1);”,构成多态,执行结果是B->1。p->func()不构成多态,是派生类调用自己的成员函数,执行结果是B->0。
协变(了解)
派生类重写基类虚函数时,可以与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。协变的实际意义并不大,所以我们了解一下即可
析构函数的重写
分析此段代码有什么问题?
只能保证派生类B的基类成员部分释放掉,但自己的成员没有释放。那怎么解决呢?多态!
将基类和派生类的析构函数前加virtual,可还不满足多态的条件(函数名不同),为此C++的创造者设立了一个规则,编译后析构函数的函数名均被改为destructor,这也回答了上一部分中为什么基类、派生类的析构函数构成隐藏
关键字:override和final
C++对多态虚函数重写的要求比较严格,如果考虑不周,没有实现出多态,编译时不会报错,等到程序结果不符合预期,调试才能发现问题,C++11为了帮助用户检测是否重写,新增了override关键字
如果不想派生类重写某虚函数,可以在虚函数后面加final,格式同override。至此,我们已经知道了关键字final的两个用法,另一个是让类无法继承
总结
| 重载 | 两个函数在同一作用域 函数名相同,参数列表不同(个数不同/类型不同),返回值可以相同,也可以不同 |
| 隐藏 | 两个函数分别在继承体系的父类或子类中 函数名相同(由此不构成重写,就会是隐藏) 父类子类的成员变量名称相同也会构成隐藏(类型不同也会隐藏) |
| 重写 | 两个函数分别在继承体系的父类或子类中 函数名、参数列表、返回值相同,协变有点例外 两个函数都是虚函数 |
纯虚函数与抽象类
virtual 返回类型 函数名(参数) = 0;在虚函数的后面写上=0,则这个函数为纯虚函数,不需要写函数体(也可以有函数体,但极少用)。包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象,如果派生类继承后不重写纯虚函数,那么派生类也是抽象类。纯虚函数某种程度上强制了派生类重写虚函数,因为不重写实例化不出对象
多态的原理
虚函数表指针
从我们前面类和对象的知识来看,这段代码的结果是8,但这里不同,类中有虚函数,底层与普通类的对象不同,还多了一个_vfptr的指针(32位下4字节,64位下8字节)放在对象内存空间的开头,那么_vfptr是什么呢,它是虚函数表指针(v是virtual,f是function),它指向一个函数指针数组(节省空间,不用将所有虚函数指针存放在对象内存中),里面存放的是当前对象的所有虚函数指针。虚函数表简称为虚表
多态的机理
那是系统是如何实现基类的指针/引用来调用所指向对象的虚函数呢?C/C++调用函数有两种方式,1、函数在当前文件声明定义一起,编译器在编译时就可以直接确定函数地址;2、函数声明与定义分离,编译器在链接时确定函数地址。满足多态条件后,虚函数调用并不是这两种情况,而是运行时到指向对象的虚表中确定所调用虚函数的地址
动态绑定与静态绑定
对不满足多态条件(指针或者引用+调用虚函数)的函数调用是在编译时绑定,也就是编译时确定调用函数的地址,叫做静态绑定
满足多态条件的函数调用是在运行时绑定,也就是在运行时到指向对象的虚函数表中找到调用函数
的地址,也就做动态绑定
虚函数表
基类对象的虚函数表中存放基类所有虚函数的地址。同类型的对象共用同一张虚表,不同类型的对
象各自有独立的虚表,所以基类和派生类有各自独立的虚表
派生类由两部分构成,继承下来的基类和自己的成员,一般情况下,继承下来的基类中有虚函数表
指针,自己就不会再生成虚函数表指针。但是要注意的这里继承下来的基类部分虚函数表指针和基
类对象的虚函数表指针不是同一个,就像基类对象的成员和派生类对象中的基类对象成员也独立
的
派生类中重写的基类的虚函数,派生类的虚函数表中对应的虚函数就会被覆盖成派生类重写的虚函
数地址
派生类的虚函数表中包含,(1)基类的虚函数地址,(2)派生类重写的虚函数地址完成覆盖,(3)派生类自己的虚函数地址三个部分
虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个0x00000000标
记。(这个C++并没有进行规定,各个编译器自行定义的,VS系列编译器会再后面放个0x00000000标记,g++系列编译不会放)
虚函数存在哪的?虚函数和普通函数一样的,编译好后是一段指令,都是存在代码段的,只是虚函
数的地址又存到了虚表中
虚函数表存在哪的?这个问题严格说并没有标准答案C++标准并没有规定,VS下是存在代码段(常量区)