目录
1、什么是虚函数
1.1 什么是虚函数重写
1.2 虚函数的继承
1.3 协变
1.4 析构函数的重写
2、override和final
2.1 final
2.2 override
3、纯虚函数/抽象类
3.1 接口继承和实现继承
4、多态的原理
前言:
在C++中,多态指的是调用同一个类的成员函数时,会产生两种不同的结果,因此多态又称多种形态。
实现多态的前提是:
1、两个类必须满足继承关系。
2、必须通过基类的指针或引用去调用该类的成员函数,并且这个成员函数是被virtual修饰过的虚函数,还得完成虚函数的重写。
多态结构示意图如下:
1、什么是虚函数
被关键字virtual修饰的类成员函数就是虚函数,比如下面的BuyTicket就是一个虚函数。
class Person {
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
1.1 什么是虚函数重写
在基类和派生类中都存在一个相同的虚函数(即函数名、函数形参类型及个数、返回类型都相同),这时候把派生类中的虚函数叫做基类虚函数的重写。
注意:虚函数重写和虚函数是两个概念,即虚函数不一定构成虚函数重写,构成虚函数重写的前提是两个函数必须是虚函数。
体现虚函数重写的代码如下:
#define _CRT_SECURE_NO_WARNINGS 1#include<iostream>
using namespace std;class Person {
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:virtual void BuyTicket() { cout << "买票-半价" << endl; }
};void Func(Person& p)
{ p.BuyTicket();
}
int main()
{Person ps;Student st;Func(ps);Func(st);return 0;
}
运行结果:
从结果可以看到,虚函数重写的目的就是为了实现多态,上面代码通过同一个形参p调用两个不同的函数BuyTicket,从而达到实现不同的功能。
1.2 虚函数的继承
在继承关系中,派生类可以继承基类的成员,那么虚函数也是可以被继承的,比如拿上述代码举例,Student类中如果没有对BuyTicket函数进行virtual修饰,却同样可以实现多态的效果,原因就是派生类继承了基类的虚函数virtual,让自己的BuyTicket也变成了一个虚函数。
但是如果基类中的BuyTicket函数不是虚函数,而派生类的BuyTicket是虚函数则无非满足虚函数的重写。
1.3 协变
从上文可以得知,虚函数重写的要求是:函数名、函数形参类型及个数、返回类型都相同,则协变就是在此基础上放宽了对虚函数重写的要求,即返回类型不相同的虚函数也可以构成重写,但是返回的必须是指针或者引用,并且这两个虚函数的返回值是继承关系。
协变示例代码如下:
#define _CRT_SECURE_NO_WARNINGS 1#include<iostream>
using namespace std;class Person {
public:virtual Person& BuyTicket() { cout << "买票-全价" << endl; return *this; }
};
class Student : public Person {
public:virtual Student& BuyTicket() { cout << "买票-半价" << endl; return *this;}
};void Func(Person& p)
{ p.BuyTicket();
}
int main()
{Person ps;Student st;Func(ps);Func(st);return 0;
}
运行结果:
协变示意图如下:
1.4 析构函数的重写
析构函数的重写可以不满足协变和虚函数重写的要求,只需要对两个类的析构函数加上virtual即可实现重写。原因在于派生类和基类的析构函数都会被统一处理成destructor的函数名,这时候他们就满足了虚函数重写的要求(此时函数名相同了)。
#define _CRT_SECURE_NO_WARNINGS 1#include<iostream>
using namespace std;class Person {
public:virtual ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:virtual ~Student() { cout << "~Student()" << endl; }
};int main()
{Person* p1 = new Person;Person* p2 = new Student;delete p1;delete p2;return 0;
}
运行结果:
2、override和final
override和final的目的就是手动的对虚函数重写进行严格的检查,因为在实现虚函数重写的时候有些疏忽编译器是不会报错的,因此我们可以使用override和final严格要求虚函数的重写。
2.1 final
修饰一个虚函数,让其不能被重写。
示例代码:
#define _CRT_SECURE_NO_WARNINGS 1#include<iostream>
using namespace std;class Person
{
public:virtual void Number() final {}
};
class Student :public Person
{
public:virtual void Number() {}//由于基类在虚函数后面加了final,因此此处不能被重写
};int main()
{return 0;
}
运行结果:
2.2 override
override的作用是检查被修饰的虚函数是否重写了基类的某个虚函数。
override示例代码如下:
#define _CRT_SECURE_NO_WARNINGS 1#include<iostream>
using namespace std;class Person
{
public://virtual void Number() {}
};
class Student :public Person
{
public://Person类中没有实现Number虚函数,因此此处没有实现虚函数的重写virtual void Number()override {}
};int main()
{return 0;
}
报错原因:
3、纯虚函数/抽象类
在虚函数的声明末尾处写上“=0”,则该虚函数为纯虚函数。而这个纯虚函数所在的类称之为抽象类,即不能用该类实例化对象,并且继承该抽象类的派生类也不能实例化出对象。
纯虚函数/抽象类的写法:
#define _CRT_SECURE_NO_WARNINGS 1#include<iostream>
using namespace std;class Person
{
public:virtual void Number()=0 {}
};
class Student :public Person
{
public://virtual void Number() {}
};int main()
{Person p;Student s;//Person和Student都不能实例化出对象return 0;
}
只有重写了该抽象类中的纯虚函数,才能用派生类实例化出对象(基类还是不能实例化对象):
#define _CRT_SECURE_NO_WARNINGS 1#include<iostream>
using namespace std;class Person
{
public:virtual void Number()=0 {}
};
class Student :public Person
{
public:virtual void Number() {}//重写纯虚函数
};int main()
{//Person p;//Person依旧不能实例化对象Student s;//由于Student中的Number重写了Person的Number,因此Student可以实例化对象return 0;
}
3.1 接口继承和实现继承
实现继承就是我们所说的派生类继承了基类的成员函数,继承的是该函数的整体。而接口继承是虚函数继承,虚函数继承的只是函数的接口部分,并不是整个函数。他们是有区别的。
体现他们区别的示意图如下:
4、多态的原理
实现多态的根本原因是虚函数的重写,而在底层中,在基类部分会自动生成一个数组指针(又称虚表指针),该指针指向的数组是用来存放虚函数地址的(这个生成动作在编译阶段就已经完成了),所以该数组又称虚函数表。
因此当调用条件满足多态时,编译器实际上会到基类中找到虚表指针,然后调用的是该指针指向的虚函数,这也是为什么通过同一个接口可以实现两个不同结果。
可以通过观察一个类的大小来发现虚指针的存在:
#define _CRT_SECURE_NO_WARNINGS 1#include<iostream>
using namespace std;class Person {
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }
};int main()
{Person ps;cout << sizeof(ps) << endl;//ps的大小为4return 0;
}
通过监视窗口也可以发现虚表指针的存在:
如果是继承关系,则虚表指针存放在父类中的基类部分中,这也是为什么实现多态的条件之一是需要基类指针调用虚函数,因为虚表指针存在基类部分中,使用基类指针刚好可以与其对应。
结语:
以上就是关于多态的讲解, 多态的本质就是继承,并且需要满足虚函数重写和基类指针、引用调用,多态实际上只需要记住这两点即可,只不过在此基础上可以延申出更多的细节。最后希望本文可以给你带来更多的收获,如果本文对你起到了帮助,希望可以动动小指头帮忙点赞👍+关注😎+收藏👌!如果有遗漏或者有误的地方欢迎大家在评论区补充,谢谢大家!!