一、多态的概念
多态顾名思义就是多种形态,它分为编译时的多态(静态多态)和运行时的多态(动态多态),编译时多态(静态多态)就是函数重载,模板等,通过不同的参数来完成对不同的函数的调用(即生成多种形态)并且这个过程在编译阶段就已经完成。
动态多态是在运行时根据对象的实际类型来确定调用函数的哪个版本,完成不同的⾏为。
二、多态构成条件
1.虚函数
在类成员函数的返回类型前面添加virtual关键字即为虚函数,注意:虚函数只能定义于普通成员函数,构造函数以及类外函数不能定义虚函数。
2.虚函数的重写
虚函数重写指的是子类(派生类)对父类(基类)的重写。重写的要求是子类虚函数的返回值,函数名,参数类型必须和父类一模一样。但函数的实现逻辑不用相同。
这里如果虚函数的重写没有加virtual,但是父类加了virtual那么子类依旧保持virtual的性质,也可构成重写。
注意:对虚函数重写并没有要求缺省参数要相同,但在这里强烈建议把缺省参数设为相同值,要不然会给你带来很大的弊端和误导性。接下来我会讲到。
3.调用方式
要实现多态效果,第⼀必须是基类的++指针或引⽤++ ,因为只有基类的指针或引⽤才能既指向派⽣类对象又能指向基类;第⼆派⽣类必须对基类的虚函数重写/覆盖,重写或者覆盖了,派⽣类才能有不同的函数,多态的不同形态效果才能达到。
⽐如火车买票这个操作,当普通⼈买票时,是全价买票;学⽣买票时,是优惠买票;军⼈买票时是优先买票,我们就可以用多态来实现,如下:
cpp
#include<iostream>
using namespace std;
class ticket
{
public:
virtual void func()
{
cout << "普通票" << endl;
}
private:
};
class student:public ticket
{
public:
virtual void func()
{
cout << "学生票" << endl;
}
private:
};
void fm(ticket& pu)
{
pu.func();
}
int main()
{
ticket tk;
student stu;
fm(tk);
fm(stu);
return 0;
}
4.override和final的修饰
override关键字:因为多态的实现细节要求太多了特别是对虚函数的重写,因此C++11提供了override,可以帮助⽤⼾来检查虚函数的重写是否正确,需要放在重写的函数参数列表后面。
final关键字:如果不想子类对该虚函数进行重写的话就可以使用final关键字,放在函数名后面。
5.协变
刚才我们说了虚函数的重写一定要满足子类虚函数的返回值,函数名,参数类型必须和父类相同。协变是个例外情况。当子类重写父类虚函数时,若与父类虚函数返回值类型不同,即父类虚函数返回父类对象的指针或引用,子类虚函数返回子类对象的指针或引用,此时称为协变。协变的实际意义并不⼤,所以我们了解⼀下即可。
代码示例:
cpp
class A{};
class B :public A{};
class ticket
{
public:
virtual ticket* func()//ticket也可以是A
{
cout << "普通票" << endl;
return this;
}
private:
};
class student:public ticket
{
public:
virtual student* func() override//student也可以是B
{
cout << "学生票" << endl;
return this;
}
private:
};
析构函数的重写
父类的析构函数为虚函数,此时派⽣类析构函数只要定义,⽆论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派⽣类析构函数名字不同看起来不符合重写的规则,实际上编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统⼀处理成destructor,++所以基类的析构函数加了vialtual修饰,派⽣类的析构函数就构成重写++。
cpp
A* p1 = new A;
A* p2 = new B;
delete p1;
delete p2;
假设B是A的子类上⾯的代码如果~A(),不加virtual,那么delete p2时只调⽤A的析构函数,没有调⽤B的析构函数,就会导致内存泄漏问题。
三、纯虚函数和抽象类
在虚函数的参数列表后⾯写上=0,则这个函数为纯虚函数,纯虚函数不需要定义实现(实现没啥意义因为要被子类重写,但是语法上可以实现),只要声明即可。包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象,++如果子类继承后不重写纯虚函数,那么子类也是抽象类++。纯虚函数某种程度上强制了子类重写虚函数,因为不重写实例化不出对象。
四、多态原理
在分析对象的储存空间时,我们讲过对于同一个类实例化出的不同对象,这些对象使用的函数都是相同的,不同的是它们成员变量。所以每个对象只需要对成员变量进行储存,不用对成员函数进行储存,每一个对象使用的都是这个类的公共成员函数。
c++为虚函数单独设立了一块区域来储存虚函数的地址,叫做虚表,而这块区域其实 就是一个函数指针数组。即用来储存函数指针的一个数组。那么父类和子类就各自有一个虚表,在对象实例化的时候就会隐含(隐含:类似于成员函数里面看不见的this指针一样)着一个指针------虚表指针,来指向虚表。
cpp
#include<iostream>
using namespace std;
class A
{
public:
virtual void func(){}
};
class B
{
public:
void func(){}
};
int main()
{
A a;
B b;
cout << "a:" << sizeof(a) << endl;
cout << "b:" << sizeof(b) << endl;
return 0;
}
而虚表指针也是需要占用空间的大家可以自行地去运行一下以上代码,输出结果为:
a:4(或8,即32位与64位机器的区别)
b:1
所以在调用对象的虚函数时就跟以什么类型的形式调用无关,而是跟这个对象实例化时具体类型有关。
cpp
#include<iostream>
using namespace std;
class ticket
{
public:
virtual void func()
{
cout << "普通票" << endl;
}
};
class student :public ticket
{
public:
virtual void func()
{
cout << "学生票" << endl;
}
};
int main()
{
ticket* tk = new student;
tk->func();
return 0;
}
以上的输出结果是"学生票"。
注意:根据切片原理,子类可强制类型转化为父类,父类不能强制类型转化为子类。
五、练习
以下程序输出结果是什么()
- A:A->0
- B:B->1
- C:A->1
- D:B->0
- E:编译出错
- F:以上都不正确
这里虽然B类的func成员没有写virtual关键字,但它是由A继承下来的依旧保留virtual的性质,然后因为重写并为要求参数的缺省值相同,所以这里构成函数的重写。再来看主函数main,p调用了test,而test是A的成员函数隐含了一个const A* (this指针)的参数类型,p传到test函数满足多态,所以这里调用的是B的func。但是这里有个坑,该题的输出结果并不是"B->0",而是"B->1"。
要注意重写只是重写了函数的实现,也就是说实现多态的时候相当于调用的是父类的接口声明和子类的函数实现,而并不关心子类的函数接口声明。
所以在我们自己写虚函数的时候,最好把缺省参数设为相同值,要不然会给你带来很大的误导性。