C++ —— 多态


前言: 多态灰常重要,简单说就是,同一件事,不同的对象会拿到不同的结果,这是好理解的。本文就带大家来认识多态,使用多态,理解多态。


1. 认识多态以及使用多态

1.1 多态的概念

在编程语言和类型论中,多态(英语:polymorphism)指为不同数据类型的实体提供统一的接口。 多态类型(英语:polymorphic type)可以将自身所支持的操作套用到其它类型的值上。

举个例子:有一个父类叫做人,子类有学生,有军人。分别用父类,子类,实例出三个对象:普通人,学生A,军人B。这三个对象去做同一件事->买票,拿到的结果不一样,普通人是成人票,学生是学生票,军人是优先买票。这就是多态,同一件事,不同的对象去做,会有不同的结果(也就是调用不同的函数)。


1.2 多态的分类

多态有两大类:

  • 静态多态:比如函数重载,不同的传参会实列出不同的函数,这在编译时就能够实现
  • 动态多态:以父类的指针或引用,去调用同一个函数,传递子类对象和传递父类对象,会调用不同的函数,这在运行时实现

1.3 构成多态的条件

构成多态需要满足两个条件:

  • 通过基类的指针或引用,去调用虚函数
  • 被调用的函数必须是虚函数,而且派生类必须对基类的虚函数进行重写

需要了解:
(1) 虚函数

在成员函数前加上关键字virtual,就为虚函数。

(2) 重写

虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类
型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。

重写需要满足:

  • 必须是虚函数
  • 派生类的虚函数和基类的虚函数的返回值类型,函数名称,参数列表完全相同

注意:重写的两个例外

  1. 协变:基类返回值是基类的指针或引用;派生类返回值是派生类的指针或引用。除了返回值类型不同,其余条件不变,这也构成重写。所以说重写返回值类型一定是相同的吗?不一定。
class person
{
public:
	virtual person* buy_ticekt()
	{
		cout << "买的票,是全价" << endl;

		return this;
	}
};

class student : public person
{
public:
	virtual student* buy_ticekt()
	{
		cout << "买的票,是半价" << endl;

		return this;
	}
};
  1. 析构函数的重写

析构函数的格式为:~类名(),那么基类和派生类的类名一定不同,所以它们可以对析构函数重写吗?答案是可以重写的。这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。也就是说,基类和派生类的析构函数名称,编译时会按照同名处理。

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;
}

很明显,基类对象调用基类析构;派生类对象调用派生类析构,派生类中继承的基类数据由基类析构。
在这里插入图片描述
还有一点需要强调

只在基类将想要实现多态的函数设为虚函数就可以了,子类重写的函数前可以不加virtual,但是这种写法不够规范,我建议只要是重写那么就设为虚函数,什么情况下,可以这样使用呢?那就是析构函数的重写。


1.4 多态简易实现

有一个父类叫做人,子类有学生。去完成一件事:买票,人去买票是全价,学生去买票是半价。

#include<iostream>
using namespace std;

class person
{
public:
	virtual void buy_ticekt()
	{
		cout << "买的票,是全价" << endl;
	}
};

class student : public person
{
public:
	virtual void buy_ticekt()
	{
		cout << "买的票,是半价" << endl;
	}
};


int main()
{
	person common;

	student A;

	// 基类指针去调用
	person* ptr1 = &common;
	person* ptr2 = &A;

	ptr1->buy_ticekt();
	ptr2->buy_ticekt();

	// 基类引用去调用

	person& y1 = common;
	person& y2 = A;

	y1.buy_ticekt();
	y2.buy_ticekt();
	return 0;
}

上面的完全是满足多态的条件的,我们来看看运行结果: 有人可能对重写还是不太理解,后面讲原理时会细讲的。

在这里插入图片描述


1.5 c++的两个关键字 override 和 final

从上面重写的学习可以看出,c++对于重写的要求还是很高的。所以引入这两个关键字,进一步的规范重写。

(1) override :检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。

它是用于检查,派生类是否对某个虚函数进行了重写,只需要在子类的重写函数后面加上 override。

父类:
virtual void buy_ticekt()
	{
		cout << "买的票,是全价" << endl;

	}
子类:
virtual void buy_ticekt(int a=1) override
	{
		cout << "买的票,是半价" << endl;
	}

很明显以上,并没有完成对父类成员函数的重写,但是我加上了关键字 override,来看看运行情况:

报错了:

在这里插入图片描述
(2) final :放在类后,表示该类不能被继承 / 放在虚函数后,表示该虚函数不能再被重写

  • 在c++11前,我们想要一个类不被继承,可能用的方式是将基类的构造函数设置为私有,那么派生类继承后,会导致派生类构造对象时,无法初始化基类,而导致派生类对象构造不成功。这是一种间接的限制,这种限制也很麻烦:使得基类构造只能用基类中的函数封装一下,才能使用。

所以引入了一个关键字: final ,只需要在类名后加上 final ,此类就不能被继承了。

class person final
{

}

如果还要继承此类,毫无疑问会报错:
在这里插入图片描述

  • 放在基类的虚函数后,使得派生类不能对此虚函数进行重写:
基类:
virtual void buy_ticekt() final
	{
		cout << "买的票,是全价" << endl;

	}
派生类:
virtual void buy_ticekt() 
	{
		cout << "买的票,是半价" << endl;
	}

基类的虚函数后已经加上了关键字:final,但是子类依旧对其重写,毫无疑问会报错:

在这里插入图片描述


1.6 抽象类
1.6.1 抽象类的概念

抽象类:包括纯虚函数的类,纯虚函数:虚函数的后面加上 =0,纯虚函数不需要定义,只声明就好了。这种类被称为抽象类。

抽象类有什么作用?它是一种接口类,不需要实例化出对象,它内部的纯虚函数,需要被其子类重写后才能有价值,否则没有意义。

比如:我定义一个抽象类->car,其中的纯虚函数是显示车的品牌。昂,车的品牌多了去了,一个抽象类car,能够显示出其车牌吗?所以只需要声明此函数,没必要实现。

#include<iostream>
using namespace std;

class car
{
public:
	virtual void Car_brand() = 0;
};

class BMW : public car
{
public:
	virtual void Car_brand()
	{
		cout << "my_car_brand is BMW" << endl;
	}
};

class Benz : public car
{
public:
	virtual void Car_brand()
	{
		cout << "my_car_brand is Benz" << endl;
	}
};

int main()
{
    car* ptr1 = new BMW;

	car* ptr2 = new Benz;

	ptr1->Car_brand();

	ptr2->Car_brand();
}

在这里插入图片描述

1.6.2 接口继承和实现继承

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

抽象类,更加体现了接口继承,它的内部直接搞一个纯虚函数,只有被子类重写的命,如果不被重写那么其毫无存在意义。

纯虚函数强制要求子类对其重写,如果不被重写,那么直接报错:因为不能实例抽象类
在这里插入图片描述
这一点有点像刚讲的override ,不过override是语法层面上的检查。上面那个是本质强烈要求。


2. 多态的原理

通过上面的学习,我们认识了多态,并且简单的使用了多态,那么现在我们来讲讲多态的实现原理。上车了,同学们坐稳扶好。

2.1 虚函数表

先出一道迷惑的小题:
求:下面的类的大小

class base
{
public:
	virtual void test()
	{
		cout << "how are you" << endl;
	}

private:
	int _a;
	char _b;
	int _c;
};

一看这题简单呀,类的成员函数在代码区,所以只看成员变量,利用学过的内存对齐知识,很轻松的得到答案:12字节。好的,我们用sizeof()来 验证一下吧:
在这里插入图片描述
答案是:16 字节。昂,怎么回事?通过调试,来看看base 对象里都有什么?

在这里插入图片描述
下面的三个成员变量:_a,_b,_c不用说,主要是那个_vfptr是什么?它就是虚函数表,存的是虚函数的地址,可以将其理解成一个函数指针数组,所以它的类型是 void ** 一个二级指针,内部存的是 void *。


2.2 利用虚函数再次理解重写

重写又被称为覆盖,什么是覆盖?覆盖的是谁?如何覆盖?我们来通过虚函数表,来理解重写。

举例:我可以定义一个基类,派生类对基类中的一个虚函数进行重写

class base
{
public:
	virtual void fun1()
	{
		cout << "how are you" << endl;
	}

	virtual void fun2()
	{
		cout << "i am fine" << endl;
	}

	virtual void fun3()
	{
		cout << "thanks" << endl;
	}

protected:
	int _a;
};

class derive : public base
{
public:
	virtual void fun1()
	{
		cout << "are you ok" << endl;
	}
};

int main()
{
    base b;
	derive d;
}

可以看到,基类总共有三个虚函数,派生类对第一个虚函数进行了重写。通过调试查看重写是怎么肥事?
在这里插入图片描述
派生类对fun1(),进行了重写,所以虚函数表的第一个函数指针的值是不同的,也就是说派生类并没有继承父类的fun1(),对此fun1()进行了覆盖,本质上,子类的fun1()已经是一个新的函数,子类的虚函数表的第一个函数指针不再指向从基类继承的函数位置,而是指向了子类重写的虚函数位置。

我们再来看虚函数表的下面俩个位置,发现基类和派生类指向的虚函数位置是一样的。

所以总结:派生类 拷贝 基类的虚函数表 -> 没有重写的虚函数指向同一函数地址,进行重写的虚函数,派生类会指向它重写的函数的地址。

讲到这里我提一个问题:虚函数存在哪里?虚函数表存在哪里?

虚函数和普通成员函数一样都存在代码段,只不过虚函数的指针会存在虚函数表中;虚函数表存在具体的对象中,每个有虚函数的对象都有虚函数表。

注意:

  1. 同一类的对象的虚函数表是相同的,这个好理解,因为虚函数表存的是虚函数的地址,同一类型对象的虚函数地址是相同的,所以虚函数表也相同。
    在这里插入图片描述
    这个可以验证一下:
int main()
{
    derive d;
	derive d1;
}

在这里插入图片描述

  1. 虚函数表的末尾存的是null。
    如果在派生类中继续声明定义虚函数,派生类虚函数表中会有新定义的虚函数指针吗?
    我们可以试一下:
class derive : public base
{
public:
	virtual void fun1()
	{
		cout << "are you ok" << endl;
	}

	virtual void fun4()
	{
		cout << "not ok" << endl;
	}
};

我在派生类新增了一个虚函数fun4(),通过调试查看一下:
在这里插入图片描述
关谷神奇发现:没有显示的看到新增的虚函数在虚表中,阿尤头大了,到底存没存进去虚函数的地址,上面不是说过虚函数表中一定会有虚函数的地址吗?这是编译器在搞怪,其实是存进去了,通过内存可以看一下,虚函数表末尾存的是空指针嘛。
在这里插入图片描述
毫无疑问:红圈的地方就是我们新存的虚函数地址,我上面画到的青色框,内存从后往前读,可发现两个框的内容是一致的。用内存验证了,虚函数表存的虚函数的地址。


2.3 多态的原理

有了上面知识的铺垫,我们来正式说道多态的实现原理:多态就是不同类型的对象去做同一件事,拿到不同的结果。满足多态需要有俩个条件:基类的指针或者引用去调用虚函数,虚函数必须被派生类重写。为什么要满足这两个条件?

  1. 如果不是基类的指针或引用去调用,而是传的派生类的指针或引用,基类的对象有越界的风险,并且引用的话,基类的对象都无法传过去。我们要的就是基类的指针或引用,因为多态的实现,看的是虚函数部分,只需要看子类对基类重写的那个函数就可以了,而这个函数必然在从基类拷贝下来的虚函数表中。
  2. 虚函数必须被重写也好理解,不重写那就不叫多态,实现和基类一样的功能,那还有设计成虚函数的必要吗?

多态的实现靠的就是虚函数表,通过虚函数表找到,重写的虚函数,从而实现不同于基类的功能。

我们回到最开始举的例子->不同人买票,这样大家就更懂了:

class person 
{
public:
	virtual void buy_ticekt() 
	{
		cout << "买的票,是全价" << endl;

	}
};

class student : public person
{
public:
	virtual void buy_ticekt() 
	{
		cout << "买的票,是半价" << endl;
	}
};

int main()
{
    person common;

	student A;

	// 基类指针去调用
	person* ptr1 = &common;
	person* ptr2 = &A;

	ptr1->buy_ticekt();
	ptr2->buy_ticekt();

	// 基类引用去调用

	person& y1 = common;
	person& y2 = A;

	y1.buy_ticekt();
	y2.buy_ticekt();
}

通过调试,来看看多态的实现过程:

(1) 重写后,也可以看到,重写的虚函数也显示的标明出了类域
在这里插入图片描述
(2) 不同的对象会通过虚表,从而调到不同的函数

因为是用的基类的指针或引用,所以派生类和基类的指针或引用都可以使用多态,编译器一看是基类的指针或引用,根本不管你是基类赋值的指针,还是派生类赋值的指针,我一视同仁,都只看基类继承下来的部分。

画图讲一下吧,干讲难理解:
在这里插入图片描述

  1. ptr1指向的是基类对象,编译器管你是啥对象,你就是个person指针,你只能以person的视角来调用,要实现多态,我就去你指向的对象的虚函数表中找到对应的虚函数地址:
    在这里插入图片描述

  2. ptr2指向的派生类对象,编译器同样不管你是啥对象,依旧看出person指针,只能以person视角来调用,要实现多态,我就去你指向的对象的虚函数表中找到对应的虚函数地址:
    在这里插入图片描述

  3. 然后有了函数地址,当然就会调用对应的函数
    在这里插入图片描述
    在这里插入图片描述
    讲到这里,不知道大家看出来没,动态多态的实现是运行时,才完成的,它是运行时才完成的对应调用。


3. 多继承的多态(了解即可)

多继承的多态,啧啧,多继承我就有点头大了,里面的菱形继承更是头大,你还搞一个多继承的多态,啊,没关系,简单的讲讲就ok了,都得要稳稳的幸福,知识不能有太大的缺口。

我举一个例子:

class Base1 {
public:
	virtual void func1() { cout << "Base1::func1" << endl; }
	virtual void func2() { cout << "Base1::func2" << endl; }
private:
	int b1;
};

class Base2 {
public:
	virtual void func1() { cout << "Base2::func1" << endl; }
	virtual void func2() { cout << "Base2::func2" << endl; }
private:
	int b2;
};

class Derive : public Base1, public Base2 {
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
private:
	int d1;
};

base1中有func1(),base2中也有func1(),并且多继承的派生类对func1(),进行了重写,是对从哪个继承下来的fun1()进行的重写呢?派生类中没有进行重写的继承下来的虚函数如何存储呢?还有派生类中新生的虚函数又是如何存在那呢?

  • 首先解决第一个问题:

对base1以及base2中的func1()都进行了重写,而且重写后的虚函数是同一个。
在这里插入图片描述
看base1,base2中虚函数表,第一个位置就是我们重写后的func1(),发现地址尽然不同,但是它俩通过跳转最后是跳转到了同一个函数地址上,所以我们可以得到一个结论:虚函数表里存的不一定是虚函数的地址,它可能会存jmp跳转前的地址。

  • 然后第二个问题:

继承下来但没有进行重写的虚函数,会存放在继承下来对应的虚函数表中。

在这里插入图片描述
这个问题解决得比较简单。

  • 最后的问题:

派生类新增的虚函数会放在第一个继承的基类的虚函数表中,不过是最后一个虚函数指针罢了。这个我们可以通过内存来看,虚函数表的默认是null,内存就是00000,这个上面讲过。

在这里插入图片描述
对于多继承的多态,掌握这些也不少了,但如果,还是很好奇各种多继承的多态,我建议可以查阅相关的C++文献,要理解菱形继承的多态,不是一件容易的事。大家感兴趣可以自己再去研究,不过一般情况下用的少。


结尾语: 以上就是多态的相关知识,大家有问题可以评论或者私信,还有欢迎大佬来此斧正。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

动名词

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

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

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

打赏作者

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

抵扣说明:

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

余额充值