【C++】-多态的语法细节详解

在这里插入图片描述
💖作者:小树苗渴望变成参天大树🎈
🎉作者宣言:认真写好每一篇博客💤
🎊作者gitee:gitee
💞作者专栏:C语言,数据结构初阶,Linux,C++ 动态规划算法🎄
如 果 你 喜 欢 作 者 的 文 章 ,就 给 作 者 点 点 关 注 吧!


前言

这篇我们开始讲解关于多态的语法细节,C++难学的第一个点在类和对象刚开始,第二个点就是在继承和多态,我们已经度过第一个点了,接下来要迈进我们第二个难关,大家要耐下心来进行学习,博主也会梳理好逻辑,方便大家学习,话不多说我们开始学习多态。


本章重点

  1. 多态的概念
  2. 多态的定义及实现
  3. 抽象类

一、多态的概念?

我们C++是一个面向对象的语言,我们通过类描述一个个的对象,但是每个对象可能有多种状态,多态是建立在继承的,我们来看看多态的例子:

1.1 概念

多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
在这里插入图片描述
第一个例子: 比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人买票时是优先买票

第二个例子: 再举个栗子: 最近为了争夺在线支付市场,支付宝年底经常会做诱人的扫红包-支付-给奖励金的活动。那么大家想想为什么有人扫的红包又大又新鲜8块、10块…,而有人扫的红包都是1毛,5毛…。其实这背后也是一个多态行为。支付宝首先会分析你的账户数据,比如你是新用户、比如你没有经常支付宝支付等等,那么你需要被鼓励使用支付宝,那么就你扫码金额 = random()%99;比如你经常使用支付宝支付或者支付宝账户中常年没钱,那么就不需要太鼓励你
去使用支付宝,那么就你扫码金额 = random()%1;总结一下:同样是扫码动作,不同的用户扫得到的不一样的红包,这也是一种多态行为。ps:支付宝红包问题纯属瞎编,大家仅供娱乐。

通过这两个例子,我们应该知道为什么要有多态,是因为我们实现世界中有这种多种状态的情况

二. 多态的定义及实现

2.1多态的构成条件

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了Person。Person对象买票全价,Student对象买票半价。


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 p;
	Student s;
	Func(p);
	Func(s);
	return 0;
}

在这里插入图片描述
这里引入一个概念:虚函数

虚函数:即被virtual修饰的类成员函数称为虚函数。

并且静态的函数不能当成虚函数,虚函数不能定义和声明分离,只要普通函数可以当成虚函数去使用

通过这个例子相信大家应该知道多态的基本用法了,我们来看看构成多态的基本条件:

  1. 必须通过基类的指针或者引用调用虚函数(通过对象去调用时不行的)讲底层的时候在进行介绍
    在这里插入图片描述

  2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
    在这里插入图片描述

基类的的函数必须加virtual,变成虚函数,子类必须重写基类的虚函数

虚函数重写的细节:

  1. 派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的
    返回值类型、函数名字、参数列表完全相同(即类型相同就行)),称子类的虚函数重写了基类的虚函数。
    在这里插入图片描述
  2. 子类的重写虚函数前面可以不用加virtual,但是建议加上(后面会介绍这个可能为析构函数为产生的)
    在这里插入图片描述

虚函数重写的两种例外:

  1. 协变(基类与派生类虚函数返回值类型不同)
    派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回必须任意基类对象的指针或者引用,派生类虚函数返回必须任意派生类对象的指针或者引用时,称为协变。(了解),引用我就不演示了效果是一样的。
    在这里插入图片描述
  2. 析构函数的重写(基类与派生类析构函数的名字不同)

在讲解这个例外之前在补充一个知识点,实现多态后:多态的调用看的是指向的对象,而不是看类型,而普通对象的调用时是看当前调用者的类型,可以简单理解,上面的多态通过指针或者引用去调用就是多态的调用,使用对象去调用就是对象的调用,想要达到通过指向的对象去调用就必须先形成多态才可以。(在讲解底层的时候会讲到,也就是分析为什么多态需要的指针或者引用去调用而通过对象去调用不行)

有了上面的补充我们来看例子:

class A
{
public:
	virtual ~A(){cout << "~A()" << endl;}
};
class B:public A
{
public:
	virtual ~B(){	cout << "~B()" << endl;}
};
int main()
{
	A a;
	B b;
	return 0;
}

在这里插入图片描述

在继承的使用那一篇说到过,我们析构函数都是先析构子在析构父,那我们上面这个看着有点奇怪,两个函数名不是一样的,而且没有返回值,这应该没有构成重写吧??所以这是一个例外,析构函数的重写可以没有返回值,而且在编译的时候父子类的析构函数名都被统一处理为了destructor,所以函数名也是一样的,够成虚函数的重写

如果不是虚函数的重写会发生情况??
在这里插入图片描述
我们看到运行结果一样的,有的人就会说那是不是虚函数都是一样的,那为什么要使他变成虚函数呢??
原因是要适应下面的场景:
在这里插入图片描述

我们发现它没有去调用B类的析构函数,p没有进行释放,造成内存泄漏了,为什么会这样??
我们的delete b实际上是两份构成b->destructor()+operator delete
我们new B一个对象,赋值给A*,它去调用函数是普通对象的调用,因为没有实现多态,看调用者的类型为A*,所以去调用了A类的析构函数,就永远调不到B类的析构,我们只要使父类的析构函数是虚函数,因为编译器本身会把所有类的析构函数统一处理成相同名字的函数,只要父类函数是虚函数,子类加不加virtual都构成虚函数的重写,此时就构成多态了,那么此时又是指针或者引用去调用,那么指向哪个对象就去调用哪个的函数
在这里插入图片描述
所以通过这个例子也说明了,多态调用和普通调用是不一样的,等底层原理的时候会给大家一一解释的。也解决了析构函数可以是虚函数,为什么要变成虚函数,原因就是为了解决上面的出现的情况,也解决了我上面说过为什么父类的virtual必须加,而子类的不需要,因为设计的时候,析构函数就不用加,也是防止写的人忘记加而导致的问题,后面为了保持一致,所以虚函数在重写的时候可加可不加。

所以我们在实现类继承或者多态的时候尽量都把基类的virtual给加上,防止出错

2.2 C++11 override 和 final

从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了override和final两个关键字,可以帮助用户检测是否重写。

  1. final:修饰虚函数,表示该虚函数不能再被重写
    在这里插入图片描述

博主认为一个虚函数不能被重写,那么它是虚函数的意义就没有了,虚函数就是为了多态的,多态就是需要虚函数的重写的,所以final用到的也特别少,反而下面这个关键字用到还是比较多的。

final还有一个用途:
设计一个类,不能被继承按照以往的案例,
(1)我们可以将构造函数私有化

class A
{
private:
	A(){}
};

我们没有办法创建类对象了,我们可以向外提供一个接口:

class A
{
public: 
	A createinit()
	{
		return A();
	}
private:
	A(){}
};

就面临一个问题,我们的createinit需要通过对象去调,而这个函数就是为了创建对象的,所以我们可以设置成静态的,通过类名去调用:

class A
{
public: 
	static A createinit()
	{
		return A();
	}
private:
	A(){}
};
int main()
{
	A::createinit();
	return 0;
}

(2)将析构函数私有化,就调用不到析构函数了
(3)使用final进行修饰类
在这里插入图片描述

final必须放在后面。

  1. override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。
    在这里插入图片描述

就是怕你想要fun是多态,但是基类忘记写了,代码少你可以一下子看出来,代码多了就不好看了。

2.3 重载、覆盖(重写)、隐藏(重定义)的对比

覆盖是底层的说法,重写是语法的说法
在这里插入图片描述

三、抽象类

3.1 概念

在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

class Car
{
public:
	virtual void Drive() = 0;
};
class Benz :public Car
{
public:
	 virtual void Drive()
	 {
		 cout << "Benz-舒适" << endl;
	 }
};
class BMW :public Car
{
public:
	 virtual void Drive()
	 {
		 cout << "BMW-操控" << endl;
	 }
};
void Test()
{
	 Car* pBenz = new Benz;
	 pBenz->Drive();
	 Car* pBMW = new BMW;
	 pBMW->Drive();
}

在这里插入图片描述

3 .2 接口继承和实现继承

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数

抽象类是多态的一种约束,你是抽象类,就必须使用多态,不然都实例化不出对象,就一点用都没有

四、总结

说到这里我们多态的具体使用细节就到这里了,说实话多态形成的条件还是特别多的,那条件多,就更来说明底层设计的更复杂,需要很多条件才能适应它涉及的场景,博主猜想:先设计出来多态,然后使用的时候发现需要这些条件才能满足,才导致多态的语法细节比较多,但是没事,博主会给大家讲解明白的,等我们学习了底层原理之后,就会恍然大悟,前面一会语法为什么要这样就解决了,我们今天就到此为止,我们下篇再见吧。。
请添加图片描述

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

橘柚!

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值