多态,顾名思义便是多种形态的意思。从现实来说,如买票这种行为,学生票的半价和成人票的原价表现出的是两种不同的状态,那么从语言来看,便是一段代码,不同的对象执行会出现不同的结果。
一、多态的构成及实现
1.多态构成条件
开文讲了多态的概念,那么在语言中,多态是如何构成的呢?请看下文,多态的定义要满足如下的条件:
1. 必须通过基类的指针或者引用调用虚函数
2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
看了这两个条件,是不是有一些😵了,明明讲的多态,怎么又出现了虚函数?害,其实这并不冲突,多态便是通过虚函数来实现的,下面我们就来认识认识虚函数。
2.虚函数
虚函数,如果对继承的知识有印象的话,相信会想起虚继承吧,没错,这两个没有关系!不过原理有些相似,在第二部分各位可以对比一下。废话不多说了,下让我们来看看什么是虚函数。
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl;}
};
如上,便是虚函数,看上去就是一个用virtual修饰过的函数,但是对多态来说,只有一个虚函数怎么会构成多态呢?要构成多态,就肯定需要多个虚函数了。
3.虚函数的重写
虚函数的重写(覆盖):当派生类中有一个跟基类完全相同的虚函数时(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),便称子类的虚函数重写了基类的虚函数。
下面来看一看如何具体构成虚函数的重写:
class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-半价" << endl; }
/*void BuyTicket() { cout << "买票-半价" << endl; }*/
/*注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因
为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议
这样使用*/
};
4.运行时多态
好了,前面既然已经铺垫了这么多,怎么说也该看看多态长什么样子了吧?下面我们根据第3小节的代码试一试多态:
void Func(Person& p)
{
p.BuyTicket();
}
int main()
{
Person ps;
Student st;
Func(ps);
Func(st);
return 0;
}
运行结果:
可以看到,同一个函数Func,在不同对象调用时,出现了不同的运行结果,那么这种情况便被称为多态!
二、多态原理
看到了此处,相信各位在第一部分见识了多态后,心中难免会升起一股好奇,多态的底层到底是如何实现的呢?本人再次提出虚继承,如果各位有着虚继承概念,那么就会发现,虚函数的重写与虚继承有着异曲同工之妙。下面我们来看一个小小的问题:
// 笔试题:sizeof(Base)是多少?
class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _b = 1;
};
运行结果:
可以看到,sizeof后并非为4字节大小,而是8字节,那么这说明在这个类中,肯定多出了一个4字节的类型。 那这个4字节的类型是什么?我们继续往下看:
1.虚表指针(虚函数表指针)
没错,这个多出来的4字节是一个指针,被称为虚表指针,该指针一般被命名为_vfptr,而这个指针指向的那一块地址被称为虚表.但虚表和虚继承中的虚基表不同,在虚表中储存的都是虚函数指针。
而通常情况下,虚表指针被放在对象的最前端(与平台有关,有的平台放在最后端),且一个含有虚函数的类中至少有一个虚表指针。
2.虚表的重写
虚表的重写,对应着虚函数的重写,即为什么会发生虚函数的重写,是因为虚表发生了重写,当派生类中有着与基类相同的虚函数时,派生类便会在从基类继承下来的虚表内容中,找到那个相同”虚函数“,将其覆盖为自己的”虚函数“。至于为什么用引号,记住,虚表中存的是[虚函数指针]!
3.运行时多态原理
到了此处,多态的大部分原理已经解释清楚,不过对于构成多态的第一个条件“为什么要基类的指针或者引用”,还有些细节没有解释呢?不过我相信有的小伙伴通过上文的一些知识点,已经猜了出来。使用基类是因为:1.虚表指针存放于基类作用域,2.基类无法转为派生类,3可能存在多个派生类。使用指针或引用是因为形参只是实参的复制。
下面,看图再走一边过程,重新构成一次多态,相信你会理解的更深刻:
总体过程:因为不同的对象中存着不同的虚表(派生类的虚表已重写),所以当不同对象调用相同虚函数时,实际上是从各自虚表中调用了该虚函数。
三、多态干货小知识
1.协变和析构函数的重写
对于这两者,不满足虚函数的重写条件也可以发生重写。
(1)1. 协变(基类与派生类虚函数返回值类型不同)
派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指
针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。
//可重写
class A{};
class B : public A {};
class Person {
public:
virtual A* f() {return new A;}
};
class Student : public Person {
public:
virtual B* f() {return new B;}
};
(2) 析构函数的重写(基类与派生类析构函数的名字不同)
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,
看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处
理,编译后析构函数的名称统一处理成destructor。
class Person {
public:
virtual ~Person() {cout << "~Person()" << endl;}
};
class Student : public Person {
public:
virtual ~Student() { cout << "~Student()" << endl; }
};
// 只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函
//数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。
int main()
{
Person* p1 = new Person;
Person* p2 = new Student;
delete p1;
delete p2;
return 0;
}
2.C++11 override 和 final
C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失。因此:C++11提供了override和final两个关键字,可以帮助用户检测是否写。
(1) final:修饰虚函数,表示该虚函数不能再被重写。
class Car
{
public:
virtual void Drive() final {}
};
class Benz :public Car
{
public:
virtual void Drive() {cout << "Benz-舒适" << endl;}
};
(2)override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
class Car{
public:
virtual void Drive(){}
};
class Benz :public Car {
public:
virtual void Drive() override {cout << "Benz-舒适" << endl;}
};
3.抽象类
在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。
//该类无法实例化,只有被继承且重写后才能实例化
class Car
{
public:
virtual void Drive() = 0;
};
*接口继承: 即基类函数的返回值-函数名-参数列表会被派生类继承(缺省值也会继承),不会重写。
4.多继承的虚表
多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中,这里依旧遵循着先继承先操作的原则。如下图:
5.动态绑定与静态绑定
1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载
2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。