目录
一.多态的概念
二.多态的定义及实现
二.1多态的构成条件
二.2虚函数
1.虚函数的写法
2.虚函数的重写/覆盖
3.协变
二.3析构函数的重写
二.4override和final关键字
编辑二.5重载/重写/隐藏的对比
三.多态的运行原理(一部分)
四.多态的常见问题!!!!
一.多态的概念
多态(polymorphism)的概念:通俗来说,就是多种形态。多态分为编译时多态(静态多态)和运⾏时多 态(动态多态),这里我们重点讲运行时多态,编译时多态(静态多态)和运⾏时多态(动态多态)。编译时 多态(静态多态)主要就是函数重载和函数模板,他们传不同类型的参数就可以调用不同的 函数,通过参数不同达到多种形态,之所以叫编译时多态,是因为他们实参传给形参的参数匹配是在 编译时完成的,我们把编译时⼀般归为静态,运行时归为动态。 运行时多态,具体点就是去完成某个个为(函数),可以传不同的对象就会完成不同的⾏为,就达到多种 形态。比如买票这个行为,当普通⼈买票时,是全价买票;学生买票时,是优惠买票(5折或75折);军 ⼈买票时是优先买票。再比如,同样是动物叫的⼀个行为(函数),传猫对象过去,就是”(>^ω^<)喵喵“,传狗对象过去,就是”汪汪“。
二.多态的定义及实现
二.1多态的构成条件
多态是⼀个继承关系的下的类对象,去调用同⼀函数,产生了不同的行为。
实现多态还有两个必须重要条件:必须是基类的指针或者引用调用虚函数 被调用的函数必须是虚函数,并且完成了虚函数重写/覆盖。
说明:要实现多态效果,第⼀必须是基类的指针或引用,因为只有基类的指针或引用才能既指向基类 对象又指向派生类对象;第⼆派生类必须对基类的虚函数完成重写/覆盖,重写或者覆盖了,基类和派生类之间才能有不同的函数,多态的不同形态效果才能达到。
示例
二.2虚函数
虚函数是构成多态的必要条件,那么怎样写虚函数,才能够正确实现多态呢?
1.虚函数的写法
类成员函数前面加virtual修饰,那么这个成员函数被称为虚函数。注意非成员函数不能加virtual修饰。(友元函数不能加virtual,因为友元函数不是成员函数)(stastic也不能和virtual同时使用)
2.虚函数的重写/覆盖
虚函数的重写/覆盖:派生类中有⼀个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值 类型、函数名字、参数列表完全相同,后续我们将其称之为三同)(注意!!函数的缺省值可以不同!!!),称派生类的虚函数重写了基类的虚函数。
注意:
1.在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承 后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用不过在考试选择题中经常会故意买这个坑让你判断是否构成多态。
2.注意!!构成多态时可以看作使用被继承的类的函数头+自己的实现,所以如果两个虚函数的缺省值不同,缺省值用的是被继承的类的,构成重写时不要求缺省值相同,形参的名字也可以不同
示例:
上述代码在构成多态时,虽然调用的是子类的方法,但是由于使用的是父类的函数头,所以缺省值为1,输出的是B->1
3.协变
正常情况下,我们实现多态需要满足三同,派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。协变的实际意义并不大,所以我们了解⼀下即可。
二.3析构函数的重写
基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析 构函数构成重写,虽然基类与派生类析构函数名字不同看起来不符合重写的规则,实际上编译器对析 构函数的名称做了特殊处理,编译后析构函数的名称统⼀处理成destructor,所以基类的析构函数加了 vialtual修饰,派生类的析构函数就构成重写。
下面的代码我们可以看到,如果~A(),不加virtual,那么delete p2时只调用的A的析构函数,没有调用B的析构函数,就会导致内存泄漏问题,因为~B()中在释放资源。
(只要这个类想构成多态,那么就把父类的析构设置成虚函数,否则肯能会造成析构调用错误,可能会发生内存泄漏的情况)
二.4override和final关键字
C++对虚函数重写的要求比较严格,但是有些情况下由于疏忽,比如函数名写错参数写错等导致⽆法构成重写,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此C++11提供了override,可以帮助用户检测是否重写。如果我们不想让派生类重写这个虚函数,那么可以用final去修饰。
注意,这两个虚函数的函数名是不一样的,所以他们不构成函数重写,有了override检查后,发生报错
二.5重载/重写/隐藏的对比
二.6纯虚函数和抽象类
在虚函数的后面写上=0,则这个函数为纯虚函数,纯虚函数不需要定义实现(实现没啥意义因为要被 派⽣类重写,但是语法上可以实现),只要声明即可。包含纯虚函数的类叫做抽象类,抽象类不能实例 化出对象,如果派生类继承后不重写纯虚函数,那么派生类也是抽象类。纯虚函数某种程度上强制了 派生类重写虚函数,因为不重写实例化不出对象。
注意:派生类必须重写纯虚函数,否则,认为这个类也是抽象类,无法实例化出对象
三.多态的运行原理(一部分)
虚函数内存对齐时会多一个虚表指针,存的是虚函数的指针,大小和机器位数有关,注意,有多个虚函数时,虚表指针的大小不会改变,依旧是地址的大小,因为虚表指针的底层是一个指针数组,它指向这个指针数组,准确的说,是虚函数指针数组
虚函数被重写之后,虚表指针会覆盖,(重写也叫做覆盖)重写是语法层面的表达,覆盖是原理层面的表达
为什么多态能够实现?首先,需要满足是一个父类的指针或者引用调用,其次,要满足虚函数的重写,重写虚函数时,虚表指针会被覆盖,虽然接收到的是一个父类的对象,实际上那是一个被切片过的子类,虚表函数是被覆盖过的,运行时,去虚表函数找到函数的地址进行调用,就实现了多态
那么为什么传一个父类的值不可以实现多态呢?传值给父类,虽然是切片,但是系统还是认为他是一个父类,用的是父类的虚表(不同类型的虚表是不一样的,切片不会传递虚表),无法实现多态,只有指针或者引用才能实现既可以指向父类,又可以指向子类,指向谁就调用谁的虚表
四.多态的常见问题!!!!
1.static与virtual不能同时使用
2.virtual关键字只在声明时加上,在类外实现时不能加
3.友元函数不属于成员函数,不能成为虚函数
4.静态成员函数就不能设置为虚函数静态成员函数与具体对象无关,属于整个类,核心关键是没有隐藏的this指针,可以通过类名::成员函数名 直接调用,此时没有this无法拿到虚表,就无法实现多态,因此不能设置为虚函数
5.在编译期间,通过传递不同类的对象,编译器选择调用不同类的虚函数(是错的!!!)不是在编译期,而应该在运行期间,编译期间,编译器主要检测代码是否违反语法规则,此时无法知道基类的指针或者引用到底引用那个类的对象,也就无法知道调用那个类的虚函数。在程序运行时,才知道具体指向那个类的对象,然后通过虚表调用对应的虚函数,从而实现多态。
6.多继承的时候,就会可能有多张虚表
7.父类对象的虚表与子类对象的虚表没有任何关系,这是两个不同的对象
8.形成多态时,即使子类中的虚函数是私有的,也可以直接调用,但是父类的虚函数不能是私有的
在父类还没有构造出来时,不满足多态的条件,不能形成多态!