C++?多态!!!

一、引言

众所周知,C++有三大特性,它们分别是封装、继承和多态,在之前的文章中已经详细介绍过封装和继承了,今天我们将一起学习多态相关的知识,如果还想了解封装、继承相关的知识,可以跳转到以下链接:

1、封装:C++?类和对象(上)!!!-CSDN博客

2、继承:C++?继承!!!-CSDN博客

二、多态的概念

1、概念

通俗来讲,多态表示多种状态,即就是说当面对不同类型、不同特点的对象时,处理一个问题时采用不同的方式从而产生不同的效果,这就是多态

2、分类

事实上,多态细分之下有两种,它们分别是静态多态和动态多态,我们常说的多态事实上代指动态多态,也就是我们今天将要主要讨论的内容,在详细了解了多态的相关知识之后我们将再来理解这两个概念

3、从实际的角度认识多态

上面我们介绍了多态的概念,这样我们可以按图索骥,大概举几个日常生活中常见的多态的实际应用:

(1).打滴滴

在打滴滴时,新人用户常常会享受较大的优惠力度,小编记得在我第一次打滴滴时,价格优惠到了4元,那天的路程还挺远的,如果放在今天可能会在十元往上,这里就用到了多态的相关知识(猜测),当一个新人用户和一个老用户同样的调用"打车"接口时,却对应了不同的优惠力度,这正好对应了多态的概念

(2).买票系统

我们日程生活中会进行各种各样的买票操作,比如各个景点或者是买回家的车票,不难发现,常见的对象会被平台分为:普通身份、学生、军人等

当这些对象同样调用买票接口时,普通身份会全家买票,学生是半价买票,军人常见的则是优先买票,很明显,不同的对象调用同一接口,产生了不同的效果,对应了多态的概念

通过以上两个常见的概念,我们可以感受到多态的相关知识是存在在我们生活中的方方面面的

三、多态的定义及实现

1、虚函数

虚函数:即就是被virtual关键字修饰的函数:

复制代码
class Person
{
public:
	virtual void buy_t()
	{
		cout << "全价购票" << endl;
	}
};

2、虚函数的重写

虚函数的重写:派生类中有一个函数跟基类的虚函数三同(即函数名、函数参数、函数返回值都相同)的函数,那么就称该派生类重写(覆盖)了基类的虚函数,例如:

复制代码
class Person
{
public:
	virtual void buy_t()
	{
		cout << "全价购票" << endl;
	}
};
class Student : public Person
{
public:
	void buy_t()
	{
		cout << "半价购票" << endl;
	}
};

以上的情况我们就说Student类重写了Person类中的buy_t函数

但是需要注意的是,虚函数重写存在以下两个例外:

(1).协变(基类与派生类函数返回值不相同)

派生类重写基类虚函数是,与基类函数返回值类型不同,当且仅当一个继承体系的返回值对应的返回了一个继承体系(并不限制一定是本地的继承体系)的指针或引用,这时候仍然构成虚函数重写,称为协变(了解即可,不推荐使用)例如:

复制代码
class A{};
 class B : public A {};
 class Person {
 public:
 virtual A* f() {return new A;}
 };
 class Student : public Person {
 public:
 virtual B* f() {return new B;}
 };
(2).析构函数的重写(基类与派生类析构函数名不相同)

如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字, 都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同, 看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处 理,编译后析构函数的名称统一处理成destructor

所以为什么要这样特殊处理析构函数,使它可以构成虚函数重写呢?,我们从下面一个例子来看:

复制代码
class Person {
 public:
 virtual ~Person() {cout << "~Person()" << endl;}
 };
 class Student : public Person {
 public:
 virtual ~Student() { cout << "~Student()" << endl; }
 };
 // 只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函
数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。
int main()
 {
 Person* p1 = new Person;
 Person* p2 = new Student;
delete p1;
 delete p2;
 }
 return 0;

上面的代码中,p1、p2都是Person*的变量,随后调用delete对这两个动态申请的空间进行释放,事实上delete对于自定义类型会调用对应类的析构函数,此时就产生了一个问题:两个空间都会调用Person的析构函数,这是我们不想看到的,我们希望的是对于p1调用Person的析构函数,而对于p2则是调用Student的析构函数

这时候我们可以认真的观察一下我们上面的需求,好像就是使用基类的指针来调用同一个函数,同时我们想让该调用动作对于不同的对象产生不同的效果,是的,这就是我们前面多态讨论过的需求,现在只有一个条件还没有满足,就是函数名并不相同,所以我们顺理成章的想到要让编译器对析构函数名进行特殊处理,这样在将基类的析构函数写为虚函数时,自然的就解决了上面的问题

3、多态的构成条件

多态是在继承关系中,不同的类对象调用同一函数,产生了不同的行为,比如Student继承了Person,这时候Person对象全价买票,Student对象半价买票,所以首先的,多态是存在在继承关系中的

在继承关系中要构成多态还有两个条件:

(1).必须通过基类的指针或者饮用调用函数

(2).被调用的函数必须是虚函数,同时派生类对基类的虚函数进行重写

下面是构成多态的一个完整例子:

复制代码
#include <iostream>
using namespace std;
//多态
class Person
{
public:
	virtual void buy_t()
	{
		cout << "全价购票" << endl;
	}
};
class Student : public Person
{
public:
	void buy_t()
	{
		cout << "半价购票" << endl;
	}
};
void func(Person& rp)
{
	rp.buy_t();
}
int main()
{
	Person p;
	Student s;
	func(p);
	func(s);


	return 0;
}

这一段代码的运行结果如下:

四、C++11中提供的两个相关的关键字:override和final

经过上面的讲解,我们发现,C++中构成重写从而构成多态的过程时非常严格的,而在平常的代码工作中我们很容易会犯一些错误,比如:大小写的问题、字母顺序的问题,这些问题产生时是很难发现的,对于这些问题,只是没有构成重写,但并没有编译、链接的错误,不会报错,非常头疼,所以在C++11中我们提供了override和final两个关键字,它们两个可以帮助我们检查这一类问题

1、final:该关键字有两个作用

(1).修饰虚函数,被修饰的函数不能被重写:
复制代码
class Person
{
public:
	virtual void buy_t  ()final//final修饰了该函数
	{
		cout << "全价购票" << endl;
	}
};
class Student : public Person
{
public:
	void buy_t()//这个位置会报错:无法重写"final"函数 "Person::buy_t"

	{
		cout << "半价购票" << endl;
	}
};
(2).修饰一个类,被修饰的类不能被继承
复制代码
#include <iostream>
using namespace std;
//多态
class Person final//使用final修饰这个类
{
public:
	virtual void buy_t()
	{
		cout << "全价购票" << endl;
	}
};
class Student : public Person//这个位置会报错:不能将"final"类类型用作基类
{
public:
	void buy_t()

	{
		cout << "半价购票" << endl;
	}
};

2、override:检查派生类函数是否重写了基类某个虚函数,如果没有就报错

复制代码
class Person
{
public:
	virtual void buy_t()
	{
		cout << "全价购票" << endl;
	}
};
class Student : public Person
{
public:
	void buy_tx() override//override修饰该函数
		//该位置报错:使用override修饰的函数不能重写基类成员

	{
		cout << "半价购票" << endl;
	}
};

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

六、抽象类

1、概念

在虚函数的函数头之后加上=0,此时该函数被称为纯虚函数,包含纯虚函数的类叫做抽象类(也叫做接口类),抽象类不能实例化出对象。派生类继承之后也不能实例化出对象,只有重写了纯虚函数,派生类才能实例化出对象,纯虚函数规范了派生类必须重写,它更能体现出接口继承

下面的代码体现出了这种接口继承的思想:

复制代码
#include <iostream>
using namespace std;
//多态
class Person
{
public:
	virtual void buy_t() = 0;
	
};
class Student : public Person
{
public:
	void buy_t()
	{
		cout << "半价购票" << endl;
	}
};
class Teacher :public Person
{
public:
	void buy_t()
	{
		cout << "十倍价钱购票" << endl;
	}

};
void func(Person& rp)
{
	rp.buy_t();
}
int main()
{
	Teacher t;
	Student s;
	func(t);
	func(s);


	return 0;
}

下面是以上代码的执行结果:

2、接口继承和实现继承

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

七、多态的原理

1、虚函数表

(1).引入
复制代码
// 这里常考一道笔试题:sizeof(Base)是多少?
class Base
 {
 public:
 virtual void Func1()
 {
 cout << "Func1()" << endl;
 }
 private:
 int _b = 1;
 };

我们先通过打印的方式看一下这个问题的结果是多少?

(2).解决问题

可以看到,结果输出了8(这里要强调一下,小编实在x86的环境下输出的,环境或者平台改变可能会影响结果),这是为什么呢?或许含有虚函数的类对象进行了一些特殊处理?接下来我们通过调试的方法来看一下该类对象模型是怎样的:

经过上面的调试窗口我们知道,原来在Base类中除了_b成员,还多一个__vfptr放在对象的前面(注意有些 平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代 表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数 的地址要被放到虚函数表中,虚函数表也简称虚表,那么派生类中这个表放了些什么呢?我们接着往下分析

为了符合多态的情景,我们先对上面的代码做出以下改造:

复制代码
// 针对上面的代码我们做出以下改造
// 1.我们增加一个派生类Derive去继承Base
 // 2.Derive中重写Func1
 // 3.Base再增加一个虚函数Func2和一个普通函数Func3
 class Base
 {
 public:
 virtual void Func1()
 {
 cout << "Base::Func1()" << endl;
 }
 virtual void Func2()
 {
 cout << "Base::Func2()" << endl;
 }
 void Func3()
 {
 cout << "Base::Func3()" << endl;
 }
 private:
 int _b = 1;
 };
 class Derive : public Base
 {
 public:
 virtual void Func1()
 {
 cout << "Derive::Func1()" << endl;
}
 private:
 int _d = 2;
 };
 int main()
 {
 Base b;
 Derive d;
 return 0;
 }

接下来我们一起观察这个加强版继承体系的类对象模型,从而说明派生类中的虚表有什么不同?

可以观察到:继承之后的d对象模型中分为两个部分,分别是Base部分和自己的成员,而在Base部分中也有一个_vfptr指针,这意味着d不会生成自己的虚表指针,而是以继承的形式沿用了Base类的指针,而两个指针指向的位置是不同的,这就是说两个类的虚表是不同的,事实上的确是这样的,派生类会首先继承基类的虚表,然后对于重写过的函数将新的函数指针覆盖原本的函数指针,形成了属于自己的虚表

2、多态的实现

经过上面对于虚表指针和虚表的认识,我们大概也可以想到多态究竟是如何实现的

事实上,多态的实现原理就是虚表指针存在在父子类中基类的部分,所以必须使用基类的指针或者引用调用(不能直接使用对象调用是因为对象的切片赋值会丢失信息,而指针和引用的切片赋值不会),同时通过虚表指针我们就可以找到虚表,父子类的虚表不同,找到的函数也就不同,这时候就实现了多态调用函数

3、动态绑定与静态绑定

(1). 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态, 比如:函数重载

(2). 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体 行为,调用具体的函数,也称为动态多态

八、结语

这就是本期有关多态的全部内容了,感谢大家的阅读,欢迎各位于晏、亦菲和我一起交流、学习、进步!!! 、

相关推荐
岁月栖迟4 小时前
leetcode 49. 字母异位词分组
windows·算法·leetcode
XH华4 小时前
C语言第十一章内存在数据中的存储
c语言·开发语言
你的人类朋友6 小时前
【操作系统】Unix和Linux是什么关系?
后端·操作系统·unix
AndrewHZ6 小时前
【python与生活】如何用Python写一个简单的自动整理文件的脚本?
开发语言·python·生活·脚本·文件整理
拉法豆粉6 小时前
在压力测试中如何确定合适的并发用户数?
java·开发语言
枯萎穿心攻击6 小时前
Unity VS UE 性能工具与内存管理
开发语言·游戏·unity·ue5·游戏引擎·虚幻·虚幻引擎
爱上纯净的蓝天7 小时前
迁移面试题
java·网络·c++·pdf·c#
老赵的博客7 小时前
c++ 常用接口设计
开发语言·c++
binbinaijishu887 小时前
Python爬虫入门指南:从零开始的网络数据获取之旅
开发语言·爬虫·python·其他
uzong7 小时前
半小时打造七夕传统文化网站:Qoder AI编程实战记录
后端·ai编程