目录
一、多态的定义和构成条件
1.定义
多态即多种形态,当不同对象完成某个行为时,产生的效果是不同的,例如,动物叫声,猫和狗都继承了动物,但是当调用它们的叫声时是不一样的,再如不同职业的人薪资是不一样的......
2.构成条件
我们先举一个需要多态的例子:
#include <iostream>
using namespace std;
class Shape {
public:
int width, height;
Shape(int a = 0, int b = 0)
{
width = a;
height = b;
}
void area()
{
cout << "Shape area : " << endl;
}
};
class Rectangle : public Shape {
public:
Rectangle(int a = 0, int b = 0) :Shape(a, b) { }
void area()
{
cout << "Rectangle class area :" << width * height << endl;
}
};
int main()
{
Rectangle rec(3, 4);
Shape* p = &rec;
p->area();
return 0;
}
我们期望最后算出来的值是矩形的面积,但是通过用指向矩形对象的指针调用面积函数,发现最后调用的还是基类的面积函数,但倘若面积函数是虚函数,则就可以按预期调用矩形的面积函数了。
虚函数就是被关键字virtual修饰的成员函数,并且该函数是类的非static成员函数。
所以,构成多态需要两个条件,即①通过基类的指针或者引用调用虚函数,②被调用的函数必须是虚函数,并且派生类中要对虚函数进行重写。
二、虚函数重写
虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名、参数列表完全相同),称派生类的虚函数重写了基类的虚函数。
在派生类中重写虚函数时,也可不加virtual,默认是有virtual的,但是为了可读性等原因,一般建议还是加上。
虚函数重写的两个例外:
①协变:派生类重写基类虚函数时,与基类虚函数返回类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。
②析构函数的重写:如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,因为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。
两个关键字:
①final
final修饰虚函数后,表示该虚函数不能再被重写了。
例如:
virtual void area() final
{
cout << "Rectangle class area :" << width * height << endl;
}
②override
override是用来检查派生类虚函数是否重写了基类某个函数,如果没有重写就会报错。
例如:
virtual void area() override
{
cout << "Rectangle class area :" << width * height << endl;
}
三、重载、重写、隐藏 (重定义)的比较
重载的构成条件:同一作用域,函数名相同,参数不同(类型或者个数)的函数
重写的构成条件:两个函数分别在基类和派生类的作用域,函数名、返回值类型、参数完全一致,是虚函数
隐藏的构成条件:两个函数分别在基类和派生类的作用域,函数名相同
四、静态绑定与动态绑定
1.静态绑定
静态绑定(亦早期绑定)是指在程序编译期间就已经确定了方法调用的绑定对象。
静态绑定的特点:
①在编译期间,系统根据函数调用定位到执行函数的定义体
②当通过对象/对象指针/对象引用来调用时,只能调用该对象/对象指针/对象引用所属类的成员函数,不能根据对象指针/对象引用所指实际对象来调用该类的成员函数。例如在前边的例子中,面积函数不是虚函数时,对象指针p实际上指向的是派生类对象,但是最后调用的还是基类中的面积函数。
2.动态绑定
动态绑定(亦后期绑定)是指在程序运行时才确定方法调用的绑定对象。
也就是说,在程序运行时,根据具体的类型来确定要调用的函数,例如上边例子的中,把面积函数设置为虚函数,p实际指向的是派生类对象,所以最后调用的就是派生类中的面积函数。
多态的实现正是依托于动态绑定才得以实现,接下来详细讲解是如何实现动态绑定的。
我们依旧以上面的例子来讲解,Shape类中有两个成员变量width和height,当面积函数是虚函数时,我们求Shape对象的大小:
当面积函数不是虚函数时,我们再求其大小:
可以看到,面积函数是否是虚函数是会对对象的大小造成影响的,我们知道,成员函数在代码区,不在对象中,所以用sizeof算对象的大小时是不会算入其中的,而有了虚函数后,大小发生了变化,这是因为对象中多了一个虚函数表指针。
虚函数表指针是指向虚函数表的指针,并且该指针位于类的内存顶部。
一个含有虚函数的类中都至少有一个虚函数表指针,虚函数的地址就是放于虚函数表中的。
接下来通过草图来加深理解;
几个注意点;
1)派生类会继承基类的虚函数表,虚函数表中的地址与基类一致,但是如果派生类对基类中的虚函数重新定义,则对应的虚函数指针修改为新定义的函数地址
2)多继承时,如果对于每个基类都有虚函数,那么派生类对象中会有多个虚函数表指针
3)虚函数和虚函数表都存储在代码区
所以,当使用基类指针或引用调用虚函数时,系统会先根据对象的类型来查找对应的虚函数表,然后在虚函数表中查找所调用的虚函数的地址,最后根据地址来调用相应的虚函数,也就实现了多态。