一、多态
1. 多态的概念
多态分为编译时多态(静态多态)和运行时多态(动态多态)。
编译时多态主要就是函数重载和函数模板,通过传递不同类型的参数就可以调用不同的函数,通过参数不同达到多种形态,之所以叫做编译时多态 ,是因为它们实参传递给形参的参数匹配是在编译时完成的,我们把编译时一般归为静态,运行时归为动态。
运行时多态,具体就是完成某个行为(函数),可以传递不同的对象就会完成不同的行为,就达到多种形态。
2. 多态的定义及实现
现在,我们对于多态有了理性的认识,那么应该具体如何去实现多态呢?
多态是一个继承关系下的类对象,去调用同一函数,产生不同的行为。
实现多态的条件:
1.必须是基类的指针或者引用调用虚函数。
2.被调用的函数必须是虚函数,并且完成了虚函数的重写或覆盖。
为什么必须是基类的指针或者引用呢?
因为只有基类的指针或者引用才可以既指向基类对象又指向派生类对象。
接下来该说说什么才是虚函数了。
类成员函数前面加virtual修饰,那么这个成员函数被称为虚函数。
注 :非成员函数不能加virtual修饰。


虚函数的重写/覆盖 :派生类中有一个跟基类完全相同的虚函数 (即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称派生类的虚函数重写了基类的虚函数。
注 :在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,也可以构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性 )。但是这样不规范,建议加上virtual关键字。


3. 协变
协变与多态是非常类似的 ,只是返回值的类型不同而已。
派生类重写基类虚函数时,与基类虚函数返回值类型不同,即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时(不一定是自己返回自己的指针或者引用,其它类的也可以),称为协变。
比如Person类返回的是A类的指针或者引用,Student类返回B类的指针或者引用。


也可以是Person类返回Person的指针或者引用,Student类返回Student的指针或者引用。


4. 析构函数的重写
基类的析构函数为虚函数,此时派生类的析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写。
那么,基类与派生类的析构函数虽然没有参数,没有返回值,但是析构函数的名字不一样啊!为什么也能构成重写呢?
虽然基类和派生类的析构函数名字不同看起来不符合重写的规则,实际上编译器对析构函数的名字做了特殊处理,编译后析构函数的名称统一处理成destructor。所以基类的析构函数加了virtual关键字,派生类的析构函数就构成重写。


如果基类的虚函数没有加virtual关键字,那么基类与派生类的析构函数就没有构成重写,析构时就会造成内存泄漏的问题。

5. override和final关键字
C++对于虚函数的重写要求比较严格,但是有时候重写虚函数时由于疏忽,导致函数名写错,参数写错等导致无法构成重写,而这种错误在编译时是不会报错的,所以Debug运行时才会发现错误,过于繁琐 。所以C++11提供了override,可以帮助用户检测是否重写。


如果我们不想让派生类重写这个虚函数,那么可以用final去修饰。


6. 纯虚函数和抽象类
什么是纯虚函数呢?
在虚函数的后面写上=0,则这个函数为纯虚函数,纯虚函数不需要实现,因为没有意义,但是语法上是可以实现的,只需要声明即可。
那什么又是抽象类呢?
包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象,如果派生类继承后不重写纯虚函数,那么派生类也是抽象类。



7. 多态的原理
7.1 虚函数表指针
一个包含了虚函数的类大小是多少呢?我们来看一下。

按照内存对齐来计算,Base类的大小应该是8个字节呀,怎么会是12个字节呢(x86系统下)?
如果是x64系统下呢?

这是为什么呢 ?这就不得不说到虚函数表指针了。
用包含有虚函数的类创建出来的对象会多一个虚函数表指针__vfptr放在对象的前面(放的位置与平台有关)。一个含有虚函数的类中都至少有一个虚函数表指针,因为一个类所有虚函数的地址要被放到这个类对象的虚函数表中,虚函数表也简称虚表(只有虚函数才会被放入虚函数表中)。
虚函数表指针就指向该虚函数表。
而之所以在x86和x64的系统下会有不同的结果 ,就是因为指针的大小与平台有关。
7.2 多态是如何实现的?
我们需要从底层的角度去看。



没有虚函数,未构成多态。


满足多态条件后,底层不再是编译时通过调用对象调用确定函数的地址,而是运行时到指定的对象的虚表中确定对应的虚函数的地址,这样就实现了指针或引用指向基类就调用基类的虚函数,指向派生类就调用派生类的虚函数。
7.3 动态绑定与静态绑定
什么是动态绑定和静态绑定呢?
对不满足多态条件(指针或者引用 + 调用虚函数)的函数调用是在编译时绑定,也就是在编译时确定调用函数的地址,叫做静态绑定。


满足多态条件的函数调用是在运行时绑定,也就是在运行时到指向对象的虚函数表中找到调用函数的地址,叫做动态绑定。

7.4 虚函数表
. 基类对象的虚函数表中存放基类所有虚函数的地址。同类型的对象共用同一张虚表,不同类型的对象各自拥有独立的虚表,所以基类和派生类有各自独立的虚表。


. 派生类由两部分构成,继承下来的基类和自己的成员,一般情况下,继承下来的基类中有虚函数表指针,自己就不会在生成虚函数表指针。但是要注意的是这里继承下来的基类部分虚函数表指针和基类对象中的虚函数表指针不是同一个,就像基类对象的成员和派生类对象中的基类对象成员也各自独立的。
. 派生类中重写的基类的虚函数,派生类的虚函数表中对应的虚函数就会被覆盖成派生类重写的虚函数地址。

. 派生类的虚函数表中包含 :(1)基类的虚函数地址(2)派生类重写的虚函数地址完成覆盖(3)派生类自己的虚函数地址三个部分。


. 虚函数表本质是一个存储虚函数指针的函数指针数组,一般情况下这个数组最后面放个0x00000000标 记(C++并没有规定,由各个编译器自行定义)。
最后一个问题,虚函数存在哪的 ?虚函数和普通函数一样,编译好后是一段指令,都是存放在代码段的,只是虚函数的地址又存到了虚表中。