C++中的多态(上)

🧸🧸🧸各位大佬大家好,我是猪皮兄弟🧸🧸🧸
在这里插入图片描述

一、多态的概念

通俗来说,多态就是多种形态,具体点就是在完成某个行为,当不同的对象去完成时会产生不同的状态

比如买票,普通人正常买票,学生半价买票,军人优先买票

二、虚函数

虚函数是多态的第一个条件
加了virtual就是虚函数,就会有虚表

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;}
	//虚函数只能用于成员函数
};

有了虚函数,如果构成多态,那么这里的函数就不是构成隐藏了,而是构成重写(虚函数重写/覆盖)
要求虚函数+三同:函数名,参数列表,返回值都要相同

如果不符合重写/覆盖,就是隐藏/重定义

虚函数和虚函数表,如果没有构成多态,那么没有价值,如果不是去实现多态而写出 虚函数,那么就是白白的浪费

三、破坏多态条件的现象

1.破坏多态条件一,虚函数重写/覆盖

需要破坏三同
也就是破坏返回类型,参数列表,函数名中任意一个
在这里插入图片描述
可以看出,sd对象的调用已不构成多态,原因就在于破坏了虚函数重写

2.破坏多态条件二

变成不是指针的引用或者指针去调用虚函数
在这里插入图片描述

四、 多态的两个条件

</font color=“red”>1.虚函数(不重写也是多态,只是不重写的话体现不出效果)
2.父类的指针或者引用去调用虚函数(不满足重写虚函数就当普通函数处理)

不满足这两个条件就不构成多态的原因

class A
{
public :
	void func(int val)
	{
		cout<<"A->"<<val<<endl;
	}
};
class B:public A
{
public :
	void func(int val)
	{
		cout<<"B->"<<val<<endl;
	}
};

1.如果不虚函数重写,那么父类的指针或者引用看到的就只是父类的成员函数,比如上面这个代码,如果不重写,那么不管是A的对象,引用还是指针,会发生切片/切割,那么都只能够看到A->这个结果。(重写的原理,下面讲)

2.如果不用父类,而用子类,那么只构成了隐藏/重定义,所以要么显示的去调用父类的func,看到A->的结果,要么只能调用自己的func,看到B->的结果

3.不能够使用父类的对象来完成多态,不能完成多态的原因是没有构成多态,是普通调用,但是就算是多态调用,虚表中也不对(因为同类对象用的是同一张虚表,并不是简单的切分或者切片)

五、多态的两个特例

特例一:子类对应成员函数可以不加virtual

子类的对应成员函数可以不加关键字virtual,因为是先把父类继承下来,最好加上
这是因为虚表会继承给子类,编译器会默认认为你这个函数是需要重写的,因为你父类写虚函数就是为了完成多态,不然没有意义,所以直接就默认重写。(其实真正原因是虚函数是接口继承,下一篇会讲到)

class Person
{
public:
	virtual void BuyTicket(){cout<<"买票-全价"<<endl;}
};

class Student:public Person
{
public:
	void BuyTicket(){cout<<"买票-半价"<<endl;}
};

特例二:重写的协变

重写的协变是针对三同中的返回类型来说的,要求是返回类型可以不同,但是必须是有父子关系的指针或者引用

class Person
{
public:
	virtual A*/A& BuyTicket(){cout<<"买票-全价"<<endl;}
};

class Student:public Person
{
public:
	B*/B& BuyTicket(){cout<<"买票-半价"<<endl;}
};

上面代码中的A是B的基类,这两个的顺序不能够交换

六、多态的原理

class Base
{
public:
	virtual void Func1()
	{
		cout<<"Func1()"<<endl;
	}
private:
	int _b=1;
	char _ch = 'A';
};
//sizeof(Base)的结果是12

其实,还存了一个虚函数表指针_vfptr(virtual function pointer),指向虚函数表
vftable,这是一个函数指针数组,虚函数放进虚函数表,和继承当中的虚基表有所不同,虚函数表存的是函数地址,而虚基表存的是基类对象的存储位置与当前位置的偏移量
在这里插入图片描述
那么继承的时候,会继承父类的虚函数表下来
在这里插入图片描述
可以看出,虚基表是复制了一张,然后如果没有重写的话,用的就是同一个函数(基类的成员函数)
在这里插入图片描述
当我重写了之后,会把虚函数表中存储的函数地址改成重写之后的函数。
然后父类的指针或者引用去调用的时候,找到的就是虚表/虚函数表中重写的那个函数,也就达成了多态

</font color=“red”>对于为什么多态的调用不能用父类对象,而是用父类的指针或者引用的原因如下
在这里插入图片描述
(仔细看Base b2 =d ,可以看出b2并不是通过d来直接切分/切片,而是同一类型的对象使用同一张虚表 。因为这样的机制,所以父类对象不能用来完成多态。)但是真正原因是没有构成多态,是普通调用,发生切分/切片

七、小总结

1、多态的本质原理就是:不构成多态,则调用自己应该调用的函数,构成多态,则去调用虚表中相应函数。
在这里插入图片描述
不构成多态:
如果这里用子类的对象/指针/引用去调用,因为隐藏/重定义,调用结果func2
如果用父类的对象/指针/引用,不构成多态,会去直接调用父类的函数func1
在这里插入图片描述
构成多态:
如果这里用子类的对象/指针/引用去调用,因为隐藏/重定义,调用结果func2
如果这里用父类的指针/引用,因为构成多态,找虚表,因为重写,调用结果func2
如果这里用父类的对象,所以调用func1(这里调用func1不是因为同类对象是同一张虚表,而是因为不构成多态,直接调用的是父类的func1)

2.普通调用是编译时决议,在编译或链接时就已经确定了调用的函数的地址(编译时有定义就直接拿到,而链接的时候就再去找没拿到地址的函数,比如多个源文件的链接,或者说链接阶段去共享区找动态库)

3.多态调用是运行时决议,需要在程序运行时去指向对象的虚表中找到函数的地址,进行调用(构成多态就是多态调用)

八、析构函数的重写

class Person
{
public:
	virtual ~Person()
	{
		cout<<~Person()<<endl;
	}
};
class Student:public Person
{
public:
	virtual ~Student()
	{
		cout<<~Student()<<endl;
	}
};

</font color=“red”>为了多态的析构重写,编译器把析构函数名同一成了destructor,所以能完成重写(在继承时就已经说到了这个,所以析构会构成隐藏,父类的析构会在子类的析构后自动调用)

所以建议在继承中析构直接定义成虚函数

析构函数不定义成虚函数的问题

Person* p = new Student();

如果此时析构函数不是虚函数,那么最后释放的时候我们会delete p,因为不构成多态,这只是一个普通调用(编译时决议),所以因为切片,调用的只是父类的析构,如果不把析构定义成虚函数,那么永远就只能调用父类的析构,内存泄漏

</font color=“red”>因为子类的析构调用完后会自动调用父类的析构(继承中讲过,是为了保证他的析构顺序),所以重写之后调用子类的析构destructor资源就能全部释放
在这里插入图片描述

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

猪皮兄弟

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

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

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

打赏作者

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

抵扣说明:

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

余额充值