深入解析多态:面向对象编程灵魂

🔥个人主页:胡萝卜3.0
📖个人专栏: 《C语言》、《数据结构》 、《C++干货分享》、LeetCode&牛客代码强化刷题
⭐️人生格言:不试试怎么知道自己行不行
🎥胡萝卜3.0🌸的简介:


目录
一、认识多态:面向对象编程的灵魂
1.1 多态的核心概念解释
二、多态的实现机制深度探索
2.1 多态的构成条件
2.1.1 实现多态的双重关键条件
2.2 虚函数
2.2.1 虚函数的重写/覆盖
2.3 实战演练:腾讯多态笔试题精解
2.3.1 场景A:基础多态调用
2.3.2 场景B:复杂多态场景分析
2.3.3 多态调用的决定性因素
2.4 虚函数重写的⼀些其他问题
2..4.1 协变
2.4.2 为何基类析构函数必须为虚函数?避免内存泄漏的关键
2.4.3 override和final关键字
2.4.3.1 override
2.4.3.2 final
2.5 三大概念终极对比:重载vs重写vs隐藏
一、认识多态:面向对象编程的灵魂
多态分为以下两种:

1.1 多态的核心概念解释
多态的概念:通俗来说,就是多种形态。多态分为编译时多态(静态多态)和运行时多态(动态多态),这里我们重点讲解运行时多态。
编译时多态:主要是前面我们所讲的函数重载和函数模板,我们通过传不同类型的参数就可以调用不同的函,通过函数参数的不同可以达到多种形态,之所以叫编译时多态,是因为他们实参传给形参的参数匹配是在编译时完成的

我们把编译时一般归为静态,运行时归为动态
所谓运行时多态,就是——
运行时多态,具体点就是去完成某个行为(函数),我们传不同的对象就会完成不同的行为,就可以达到多种形态。
例如:

普通人买票时,时全价买票;而学生买票时,可以优惠买票;军人买票时是优先买票~~~(我们都是买票的行为,但是不同的对象在买票时的结果是不一样的)
再比如:同样是动物叫的一个行为(函数),传猫过去是“喵喵”,传小狗过去就是“汪汪”。
ok,我们通过代码来看一下——到底什么是多态~~~
#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* ptr)
{ptr->BuyTicket();
}
int main()
{person ps;student st;func(&ps);func(&st);return 0;
}
运行结果:

为什么调用的是同一个函数,结果为啥是不一样的呢?
ok,其实这就是多态的结果,当我们调用同一个函数,但是我们传不同的对象时,就会有不同的结果

二、多态的实现机制深度探索
2.1 多态的构成条件
多态是一个继承关系下的类对象,去调用同一函数,产生了不同的行为。就比如上面代码中的student继承了person 。person对象买票全价,student对象优惠买票


2.1.1 实现多态的双重关键条件
- 必须是基类的指针或者引用调用虚函数(指针或者引用的类型是基类类型)
- 被调用的函数必须是虚函数,并且要实现多态的效果要完成虚函数重写/覆盖
注意:完成了虚函数的重写/覆盖是要实现多态效果的条件,不是构成多态的必须条件
要实现多态效果
- 第一必须是基类的指针或者引用,因为只有基类的指针或者引用才能既指向基类对象又能指向派生类对象;
- 第二派生类必须对基类的虚函数完成重写/覆盖,只有完成虚函数的重写/覆盖,基类和派生类之间才能有不同的函数,这样多态的不同形态效果才能达到
那什么叫做虚函数(和前面的虚继承有什么关系吗?)什么又是虚函数的重写/覆盖?
我们一一来看~~~
2.2 虚函数
注意:虚函数和虚继承一点关系都没有!!!这两个是不一样的东西
我们在类成员函数的前面加上virtual 修饰,那么这个成员函数就被称为虚函数(有点草率~~~)。注意:非成员函数不能加virtual 修饰,也就是说virtual只能修饰成员函数(非静态)
class person
{
public://BuyTicket函数就是虚函数virtual void BuyTicket(){cout << "买票—全价" << endl;}
};
2.2.1 虚函数的重写/覆盖
所谓虚函数的重写/覆盖就是——
派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表(参数类型)完全相同),称派生类的虚函数重写了基类的虚函数


这样就构成了虚函数的重写/覆盖~~~
那这里有个问题派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表(参数类型)完全相同),成为派生类的虚函数重写了基类的虚函数,
那我派生类中的虚函数可不可以不加virtual?——其实是可以的
在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也构成重写(因为继承后,基类的虚函数被继承下来了,在派生类中依旧保持虚函数属性)
但是这种写法不是很规范,不建议这样使用,不过在考试选择题中,经常会故意挖这个坑,让你判断是否构成多态
那这个重写是什么意思?😵💫,重写是指重写实现

所以:派生类可以不加virtual ,基类必须加virtual !!!
2.3 实战演练:腾讯多态笔试题精解
2.3.1 场景A:基础多态调用
以下程序输出结果是什么()
A. A->0 B. B->1 C:A->1 D. B->0 E:编译出错 F. 以上都不正确

正确答案:B。小伙伴们选对了吗?
很多人可能会在C、D之间犹豫,这里答案其实是B。
我们来看一下为什么会选择B呢?
其实这个和我们上面说的内容是相关的~~~

当我们可以完成上面的这些过程,我们就会毫不犹豫的选出D. B->0的选项。但是还是不对,为什么?

总结:多态调用:父类虚函数声明+子类实现 -> 构成派生类中重写的虚函数
所以,最后选择 B. B->1
2.3.2 场景B:复杂多态场景分析
以下程序输出结果是什么()
A. A->0 B. B->1 C:A->1 D. B->0 E:编译出错 F. 以上都不正确

如果还是上一道题的代码,那这个该选什么呢?
ok,那这里没有满足多态的条件,不是多态调用,所以就是一个普通调用,普通调用就看调用指针或者引用的类型,指针p的类型是B*
所以选择D. B->0
总结:我们要看好到底是多态调用,还是普通调用,多态调用看调用的对象(看指针或者引用指向的对象),而普通调用看调用的指针或者引用的类型,并且只有多态调用时才走重写的机制!!!
2.3.3 多态调用的决定性因素

2.4 虚函数重写的⼀些其他问题

2..4.1 协变
特殊情况下,派生类函数返回类型可以不同,但必须是父子类型的指针或者引用(也就是说,基类虚函数返回基类对象的指针或者引用,派生类函数返回派生类对象的指针或者引用)。
注意:协变了解即可!!!
// 协变
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;
}
运行结果:

2.4.2 为何基类析构函数必须为虚函数?避免内存泄漏的关键
ok,我们先来看基类的虚函数不加virtual的情况:
class A
{
public:~A(){cout << "~A()" << endl;}
};
class B :public A
{
public:~B(){cout << "~B()" << endl;delete _p;}
protected:int* _p = new int[10];
};
int main()
{A a;B b;return 0;
}
在正常场景中,上面的代码可以正常运行:

但如果是这种场景呢?

会不会有什么问题?

但是我这里new的是一个B的对象,应该先调用B中的析构,再调用A中的析构,但是这里直接调用A中的析构函数,只释放A,没有调用B中的析构函数,如果此时B中有资源,就会导致内存泄露问题。
为什么会有内存泄露的问题?

那该怎么解决呢?——给基类的析构函数加上virtual

加了virtual ,就是多态调用,多态调用看对象,这里new的是B的对象,会先去调用B的析构,再调用A的析构,这样就不会造成内存泄露问题。

基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类域派生类析构名字不同看起来不符合重写的规则,实际上编译器对析构函数的名称做了特殊处理,编译后析构函数的名字统一处理成destructor,所以基类的析构函数加了virtual修饰,派生类的析构函数就构成重写
从上面的代码中,我们可以看到,如果~A(),不加virtual,那么delete ptr时只会调用A中的析构函数,不会调用B中的析构函数,就会导致内存泄露问题,因为~B()中在释放资源
- 建议:
基类和派生类的析构函数都加上virtual !!!

- 总结:
析构函数建议定义成虚函数,普通函数不建议定义成虚函数(没事不要去定义虚函数,要重写的地方才定义虚函数),构造函数不建议定义虚函数(构造函数也不能定义成虚函数)
- 面试问题
一个基类的析构函数建不建议定义成虚函数?
建议。为什么?
结合上面的场景即可:一个父类的指针指向new出来的派生类对象,然后去delete这个指针
因为如果基类的析构函数不是虚函数,就不构成重写,不构成重写,就是普通调用,就有可能会造成内存泄漏,所以这里必须是多态调用,基类析构函数是虚函数就没有问题
2.4.3 override和final关键字
从上面可以看出,C++对虚函数重写的要求比较严格,但是有些情况下由于疏忽,比如函数名写错,参数写错等导致无法构成重写,而这种错误在编译期间是不会爆出的,只有在程序运行时没有得到预期结果才会有报错,这样就得不偿失了
2.4.3.1 override
因此C++提供了override 关键字,可以帮助用户检测是否重写。派生类严格检查,不是重写都会报错

正确书写:

所以派生类重写的时候加上override关键字还是很有用处的!!!
2.4.3.2 final
如果我们不想让派生类重写这个虚函数,那么就可以用final去修饰

注意:final关键字是放在基类的虚函数中的!!!
在继承章节中的final的用处是:让这个基类不能被继承
- 总结一下:

2.5 三大概念终极对比:重载vs重写vs隐藏
注意⚠️:面试时比较喜欢考!!!

详细解释一下:

