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

八、结语

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

相关推荐
南风lof3 分钟前
ReentrantLock与AbstractQueuedSynchronizer源码解析,一文读懂底层原理
后端
你不是我我35 分钟前
【Java开发日记】基于 Spring Cloud 的微服务架构分析
java·开发语言
monkey_slh36 分钟前
JS逆向案例—喜马拉雅xm-sign详情页爬取
android·开发语言·javascript
愚润求学39 分钟前
【Linux】mmap文件内存映射
linux·运维·服务器·开发语言·c++
sbc-study1 小时前
混沌映射(Chaotic Map)
开发语言·人工智能·python·算法
写bug写bug1 小时前
彻底搞懂 RSocket 协议
java·后端
就是我1 小时前
轻松管理Linux定时任务:Cron实用教程
linux·后端
橘子青衫1 小时前
深入理解Callable与Future:实现Java多线程中的异步任务处理
java·后端
Christophe Chen1 小时前
strcat及其模拟实现
c语言·算法
bobz9651 小时前
libvirt 相关 sock 整理
后端