🔥个人主页🔥:孤寂大仙V
🌈收录专栏🌈:C++从小白到高手
🌹往期回顾🌹:【C++】继承
🔖 流水不争,争的是滔滔不息。
文章目录
- 一、多态的概念
- 二、多态的定义和实现
- 实现多态的两个重要条件
- 虚函数
- 虚函数的重写/覆盖
- 三、关于多态的扩展
- 协变
- 析构函数的重写
- 四、override和final关键词
- 五、重载/重写/隐藏的对比
- 六、纯虚函数和抽象类
- 七、多态的原理
- 动态绑定与静态绑定
- 虚函数表
一、多态的概念
在C++中,多态是面向对象编程的核心概念之一,指的是同一接口可以表现出不同的行为。多态允许程序在运行时根据对象的实际类型调用相应的函数,而无需在编译时确定具体的实现。C++中的多态通常通过继承和虚函数来实现。
多态(polymorphism)的概念:通俗来说,就是多种形态。多态分为编译时多态(静态多态)和运⾏时多态(动态多态),这⾥我们重点讲运⾏时多态,编译时多态(静态多态)和运⾏时多态(动态多态)。编译时多态(静态多态)主要就是我们前⾯讲的函数重载和函数模板,他们传不同类型的参数就可以调⽤不同的函数,通过参数不同达到多种形态,之所以叫编译时多态,是因为他们实参传给形参的参数匹配是在编译时完成的,我们把编译时⼀般归为静态,运⾏时归为动态。
二、多态的定义和实现
多态是⼀个继承关系的下的类对象,去调⽤同⼀函数,产⽣了不同的⾏为。⽐如Student继承了
Person。Person对象买票全价,Student对象优惠买票。
实现多态的两个重要条件
- 必须是引用或者指针调用虚函数
- 被调用的函数必须是虚函数
注意:要实现多态效果,第⼀必须是基类的指针或引⽤,因为只有基类的指针或引⽤才能既指向派⽣类对象;第⼆派⽣类必须对基类的虚函数重写/覆盖,重写或者覆盖了,派⽣类才能有不同的函数,多态的不同形态效果才能达到。
虚函数
类成员函数前⾯加virtual修饰,那么这个成员函数被称为虚函数。注意⾮成员函数不能加virtual修饰。
class Person
{
public:virtual void piaojia(){cout << "买全价票" << endl;}
};
虚函数的重写/覆盖
虚函数的重写/覆盖:派⽣类中有⼀个跟基类完全相同的虚函数(即派⽣类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称派⽣类的虚函数重写了基类的虚函数。
注意:在重写基类虚函数时,派⽣类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派⽣类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使⽤,不过在考试选择题中,经常会故意买这个坑,让你判断是否构成多态。
下面是一个简单的多态实现
#include<iostream>
using namespace std;
class Person
{
public:virtual void piaojia(){cout << "买全价票" << endl;}
};class Student : public Person
{
public:virtual void piaojia(){cout << "买半价票" << endl;}
};void Func(Person* person)
{person->piaojia();
}int main()
{Person xue;Student cheng;Func(&xue);Func(&cheng);}
三、关于多态的扩展
协变
派⽣类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引⽤,派⽣类虚函数返回派⽣类对象的指针或者引⽤时,称为协变。
class A {};class B : public A {};class Person {public:virtual A* BuyTicket() { cout << "买票全价" << endl;return nullptr;}};
class Student : public Person {public:virtual B* BuyTicket() { cout << "买票打折" << endl;return nullptr;}};void Func(Person* ptr){ptr->BuyTicket();}int main(){Person ps;Student st;Func(&ps);Func(&st);return 0;}
析构函数的重写
基类的析构函数为虚函数,此时派⽣类析构函数只要定义,⽆论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派⽣类析构函数名字不同看起来不符合重写的规则,实际上编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统⼀处理成destructor,所以基类的析构函数加了vialtual修饰,派⽣类的析构函数就构成重写。
下⾯的代码我们可以看到,如果~A(),不加virtual,那么deletep2时只调⽤的A的析构函数,没有调
B的析构函数,就会导致内存泄漏问题,因为~B()中在释放资源。
class A
{
public:virtual ~A(){cout << "~A" << endl;}
};
class B : public A
{
public:virtual ~B(){cout << "~B" << endl;}
protected:int* _p = new int[10];
};int main()
{A* p1 = new A;A* p2 = new B;delete p1;delete p2;return 0;
}
四、override和final关键词
C++11对函数重写的要求比较高,但是有时候比较容易忽略。比如在虚写的时候函数名写错,参数写错等。前面的错误在编译的时候编译器无法查出错误,只有在运行的时候才会报错。此C++11提供了override,可以帮助⽤⼾检测是否重写。如果我们不想让派⽣类重写这个虚函数,那么可以⽤final去修饰。
虚写条件没有写好时,使用override自动报错。
使用final,派生类将无法重写这个虚类。
五、重载/重写/隐藏的对比
六、纯虚函数和抽象类
在虚函数的后⾯写上=0,则这个函数为纯虚函数,纯虚函数不需要定义实现(实现没啥意义因为要被
派⽣类重写,但是语法上可以实现),只要声明即可。包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象,如果派⽣类继承后不重写纯虚函数,那么派⽣类也是抽象类。纯虚函数某种程度上强制了派⽣类重写虚函数,因为不重写实例化不出对象。
// 抽象类 Shape
class Shape {
public:virtual void draw() = 0; // 纯虚函数
};// 派生类 Circle
class Circle : public Shape {
public:void draw() override {cout << "Draw Circle" << endl;}
};// 派生类 Rectangle
class Rectangle : public Shape {
public:void draw() override {cout << "Draw Rectangle" << endl;}
};int main() {Shape* shape1 = new Circle();Shape* shape2 = new Rectangle();shape1->draw(); // 输出: Draw Circleshape2->draw(); // 输出: Draw Rectangledelete shape1;delete shape2;return 0;
}
七、多态的原理
从底层的⻆度Func函数中ptr->BuyTicket(),是如何作为ptr指向Person对象调⽤Person::BuyTicket,
ptr指向Student对象调⽤Student::BuyTicket的呢?通过下图我们可以看到,满⾜多态条件后,底层
不再是编译时通过调⽤对象确定函数的地址,⽽是运⾏时到指向的对象的虚表中确定对应的虚函数的地址,这样就实现了指针或引⽤指向基类就调⽤基类的虚函数,指向派⽣类就调⽤派⽣类对应的虚函数。第⼀张图,ptr指向的Person对象,调⽤的是Person的虚函数;第⼆张图,ptr指向的Student对象,调⽤的是Student的虚函数。
class Person
{
public:virtual void BuyTicket() {cout << "买票全价" << endl; }
};class Student : public Person
{
public:virtual void BuyTicket() {cout << "买票打折" << endl; }};class Soldier : public Person
{
public:virtual void BuyTicket() {cout << " 买票优先" << endl; }
};void Func(Person* ptr)
{// 这⾥可以看到虽然都是Person指针Ptr在调⽤BuyTicket// 但是跟ptr没关系,⽽是由ptr指向的对象决定的。ptr->BuyTicket();}int main()
{// 其次多态不仅仅发⽣在派⽣类对象之间,多个派⽣类继承基类,重写虚函数后// 多态也会发⽣在多个派⽣类之间。Person ps;Student st;Soldier sr;Func(&ps);Func(&st);Func(&sr);return 0;
动态绑定与静态绑定
对不满⾜多态条件(指针或者引⽤+调⽤虚函数)的函数调⽤是在编译时绑定,也就是编译时确定调⽤函数的地址,叫做静态绑定。
满⾜多态条件的函数调⽤是在运⾏时绑定,也就是在运⾏时到指向对象的虚函数表中找到调⽤函数的地址,也就做动态绑定。
虚函数表
- 基类对象的虚函数表中存放基类所有虚函数的地址。
- 派⽣类由两部分构成,继承下来的基类和⾃⼰的成员,⼀般情况下,继承下来的基类中有虚函数表指针,⾃⼰就不会再⽣成虚函数表指针。但是要注意的这⾥继承下来的基类部分虚函数表指针和基类对象的虚函数表指针不是同⼀个,就像基类对象的成员和派⽣类对象中的基类对象成员也独⽴的。
- 派⽣类中重写的基类的虚函数,派⽣类的虚函数表中对应的虚函数就会被覆盖成派⽣类重写的虚函数地址。
- 派⽣类的虚函数表中包含,基类的虚函数地址,派⽣类重写的虚函数地址,派⽣类⾃⼰的虚函数地址三个部分。