目录
什么是多态?
多态,通俗的说就就是多种形态,也就是说,不同的对象去完成同一行为时会产生不同的形态。比如,买高铁票,学生的话是可以打75折的,而非学生的成年人就是全价买票了。
构成多态的条件
多态必须满足以下3个条件:
1)继承
继承是多态的基础,它允许子类继承父类的属性和方法,并可以对这些方法进行重写,从而调用同一函数表现出不同的行为(多态)。
2)必须通过基类的指针或引用调用虚函数
3)被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
下面解释一下什么是虚函数。
虚函数
被virtual修饰的类成员函数称为虚函数。下面的BuyTicket就是一个虚函数。
class person
{
public:
virtual void BuyTicket() { cout << " 买票 — 全价" << endl; }
};
虚函数的重写
虚函数的重写,也叫覆盖。派生类中有一个跟基类完全相同的虚函数(返回值类型、函数名、函数参数列表),则称子类的虚函数重写了基类的虚函数。请看下面的例子~
#include <iostream>
using namespace std;
class Person
{
public:
virtual void BuyTicket() { cout << "买票 — 全价" << endl; }
};
class Student : public Person
{
public:
virtual void BuyTicket() { cout << "买票 — 75折" << endl; }
//下面这种写法也是正确的,但不建议
//void BuyTicket() { cout << "买票 — 75折" << endl; }
};
void Func(Person& p)
{
//基类的指针或引用调用虚函数
p.BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(ps);
Func(st);
return 0;
}
上述例子的子类中完成了虚函数的重写(BuyTicket),运行结果如下:
我们可以看到,不同的对象去完成同一行为的结果是不一样的,这就是多态。
一个特例:
析构函数的重写(基类的析构函数和派生类的析构函数的名字不同)
如果基类的析构函数为虚函数,此时派生类的析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写。很显然,这两个析构函数的函数名看起来是不同的,似乎违背了重写的规则,其实不然,因为编译器在编译后析构函数的名称统一处理成了destructor。
C++11 override和final
final:修饰虚函数,表示该虚函数不能再被重写
class Car
{
public:
virtual void Drive() final {}
};
class Benz : public Car
{
public:
virtual void Drive() { cout << "Benz - 舒适" << endl; }
};
override:检查派生类的虚函数是否重写了基类的某个虚函数,如果没有重写则编译报错
class Car
{
public:
virtual void Drive() {}
};
class Benz : public Car
{
public:
virtual void Drive() override { cout << "Benz - 舒适" << endl; }
};
知道了override的用法,下面我们再来谈谈它存在的意义。
#include <iostream>
using namespace std;
class Car
{
public:
virtual void Drive() {}
};
class Benz : public Car
{
public:
virtual void drive() { cout << "Benz - 舒适" << endl; }
};
int main()
{
Benz b;
return 0;
}
在上面的代码中,我没有使用关键字override,但实际上我尝试在派生类中重写基类的的方法(重写基类的虚函数),只是不小心把大写的D写成小写的d了,但程序是可以编译通过的,毫无疑问运行结果不符合预期。
这就导致了我没能及时的发现问题,如果加上override会怎样?我们看一看吧~
#include <iostream>
using namespace std;
class Car
{
public:
virtual void Drive() {}
};
class Benz : public Car
{
public:
virtual void drive() override { cout << "Benz - 舒适" << endl; }
};
int main()
{
Benz b;
return 0;
}
编译结果:
用override关键字修饰之后,在编译阶段就发现了问题,所以说override关键字有助于我们提前发现错误并修正。
拓展:重载、重写(覆盖)、重定义(隐藏)的对比
抽象类
在虚函数的后面加上 =0,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类)。抽象类不能实例化出对象。继承自抽象类的派生类,如果派生类中不对纯虚函数进行重写,那么派生类就包含了纯虚函数,它便是抽象类,不能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
class Car
{
public:
virtual void Drive() = 0;//纯虚函数
};
int main()
{
Car c; //Car为抽象类
return 0;
}
编译结果很好的说明了抽象类是不能实例化出对象的。
接口继承和实现继承
接口继承和实现继承是继承的两种主要形式。
1)接口继承主要关注定义一组方法,但不实现它们,然后由继承它的派生类来重写这些方法。
2)实现继承(也叫功能继承)就是派生类直接使用继承自基类的方法。
普通函数的继承是一种实现继承(功能继承),派生类继承了基类的函数就可以直接使用,继承的是函数的实现(或者说继承的是函数的功能)。虚函数的继承是一种接口继承,派生类继承的是虚函数的接口,目的是为了重写,达成多态。
多态的原理
在讲多态的原理之前,需要铺垫几个新的概念,了解了必要的概念后再串起来讲多态的原理。
虚函数表(vftable)
虚函数表(简称虚表)本质上是一个「函数指针数组」,里面存放着虚函数的地址。
虚函数表指针(vfptr)
顾名思义,虚函数表指针就是指向虚函数表的指针。下面我们通过监视窗口观察。
class Base
{
public:
virtual void Func1()
{
cout << "Func1" << endl;
}
private:
int _b = 1;
};
int main()
{
Base b;
return 0;
}
在监视窗口中,可以看到__vfptr和vftable。除了_b成员,还有一个__vfptr放在对象的前面,这个就是虚函数表指针。其中v代表virtual,f代表function。
到这先进行一个简单总结:
在C++中,每个包含至少一个虚函数的类都会有一张虚函数表,这张虚函数表包含了该类所有的虚函数地址,当类对象被创建时,它们会拥有一个虚函数指针,这个指针指向类的虚函数表。顺便提一下,一个类只有一张虚函数表,所有类的对象共用同一张虚函数表。
动态绑定和静态绑定
1)静态绑定
静态绑定又称为前期绑定或早绑定,在程序编译期间确定了程序的行为。比如函数重载和函数模板的实例化(又被成为静态多态,广义上来讲,多态分为静态多态的动态多态)。
2)动态绑定
动态绑定又称为后期绑定或晚绑定,是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。
在编译阶段,编译器会判断行为是否符合多态,符合就动态绑定,不符合就选择静态绑定。
总结起来,多态的实现机制就是:通过虚函数表指针找到虚函数表,在虚函数表中找到对应的虚函数并调用,完成多态。虚函数的调用顺序是这样子的:this指针 —> vfptr —> vftable —> 虚函数。
上面总结的还是比较抽象,这里再对多态的实现原理更详细的阐述一遍:
多态的实现原理主要是通过虚函数表和虚函数表指针来实现的。每个含有虚函数的类,以及从这样的类派生出来的类,都会有一张虚函数表,这张表中存储了虚函数的地址。类实例化出的对象中都含有一个虚函数指针,指向这张虚函数表。当我们通过基类的指针或引用调用虚函数时,会通过虚函数表指针找到虚函数表,然后在表中查找并调用相应的函数。这个过程是运行时完成的,所以可以实现运行时多态。
继承与虚函数表
1)当子类继承自一个或多个基类时,它的虚函数表将包含所有继承的虚函数的地址,以及子类自己定义的虚函数地址。如果子类重写了基类的虚函数,那么子类的虚函数表中将替换为新的函数地址(覆盖)。
2)如果子类没有重写基类的虚函数,那么它们将共享相同的虚函数地址。
再谈虚函数和虚函数表
1)虚函数表的作用
虚函数表的主要作用是实现动态绑定,即在程序运行时确定调用哪个函数。当通过基类的指针或引用调用虚函数时,虚函数表提供了一种机制来查找并调用正确的函数版本。
2)虚函数通常比非虚函数的调用稍慢,因为调用虚函数需要一些额外的间接跳转。
3)虚函数还增加了对象的大小(因为每个对象需要一个虚函数表指针vfptr),同时如果把没必要写成虚函数的函数硬要写成虚函数,还会增加虚函数表的大小,所以写成普通函数就可以的函数不建议硬要写成虚函数。
完~