C++面向对象三大特性 -- 多态(重点)

目录

  • 一、什么是多态?
  • 二、多态的定义和实现
    • 2.1 虚函数
    • 2.2 虚函数的重写
    • 2.3 多态的构成条件
    • 2.4 C++11中的override和final
    • 2.5 重写(覆盖),重载,重定义(隐藏)的对比
  • 三、多态的原理
    • 3.1 虚函数表
    • 3.2 再谈多态的条件
    • 3.3 动态绑定和静态绑定
    • 3.4 单继承和多继承关系的虚函数表
      • 3.4.1 单继承中的虚函数表
      • 3.4.2 多继承中的虚函数表
  • 四、练习
  • 五、思考以下问题

一、什么是多态?

多态就是多种形态的意思,指的是同一间事情,不同的人做,会有不同的效果;比如说你想在腾讯视频开一部电影,但是这部电影需要VIP特权才能看,而你不是VIP,当你点击观看视频时它检测到你不是VIP会员它就不让你看了,而别人是VIP的账号点击观看就能看完整版的视频。这就叫做多态。

二、多态的定义和实现

2.1 虚函数

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

2.2 虚函数的重写

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

2.3 多态的构成条件

在继承体系中构成多态必须满足两个条件:
1、必须由父类的指针或者引用调用虚函数。
2、被调用的函数必须是虚函数,并且子类必须对父类的虚函数完成重写。
虚函数重写的条件:
虚函数重写的条件本来是规定:虚函数+三同,但是有一些例外。
1、父子类的函数都需要同时为虚函数,原则上都需要加上virtual关键字,父类的函数前必须加上virtual关键字才认为是虚函数,但是子类的虚函数可以不加virtual,原因是子类是继承了父类的成员变量和成员函数的,父类的函数是虚函数,可以认为子类的这个函数也是虚函数,但是建议父子类函数都加上virtual。
2、原则上一定要满足三同:函数名,参数(完全相同),返回值必须相同;但是有一个例外就是协变,协变规定返回值可以不同,但是必须是父子类关系的指针或者引用,并且需要同为指针或者同为引用,不能一个返回指针,一个返回引用。

一、不构成多态

//不构成多态
class Person
{
public:
	void func()
	{
		cout << "Person::func()" << endl;
	}
};
class Student: public Person
{
public:
	virtual void func()
	{
		cout << "Student::func()" << endl;
	}
};

int main()
{
	//父类的引用调用函数,父类的引用引用父类对象
	Person p;
	Person& rp = p;
	p.func();

	//父类的指针调用函数,父类的指针指向父类对象
	Person* pp = &p;
	pp->func();
	
	//父类的引用调用函数,父类的引用引用子类对象
	Student s;
	Person& rs = s;
	rs.func();

	//父类的指针调用函数,父类的指针指向子类对象
	Person* sp = &s;
	sp->func();

	return 0;
}

在这里插入图片描述

二、构成多态

//构成多态
class Person
{
public:
	virtual void func()
	{
		cout << "Person::func()" << endl;
	}
};
class Student: public Person
{
public:
	//虚函数的重写
	virtual void func()
	{
		cout << "Student::func()" << endl;
	}
};

int main()
{
	//父类的引用调用函数,父类的引用引用父类对象
	Person p;
	Person& rp = p;
	p.func();

	//父类的指针调用函数,父类的指针指向父类对象
	Person* pp = &p;
	pp->func();
	
	//父类的引用调用函数,父类的引用引用子类对象
	Student s;
	Person& rs = s;
	rs.func();

	//父类的指针调用函数,父类的指针指向子类对象
	Person* sp = &s;
	sp->func();

	return 0;
}

在这里插入图片描述

需要特别注意一下析构函数的重写(基类和派生类析构函数的函数名不同)
如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。它们的函数名不相同,看起来违背了重写的规则,其实编译器对析构函数的名称是做了特殊处理的,编译后析构函数的名称统一处理成destructor,所以父子类的析构函数的函数名是相同的,无参也无返回值,所以构成重写。
析构函数是否需要是虚函数?需要。
为什么?原因如下:

如果析构函数不是虚函数,即不构成多态:


class Person
{
public:
	//析构函数不构成多态
	/*virtual */~Person()
	{
		cout << "~Person()" << endl;
	}
};
class Student : public Person
{
public:
	~Student()
	{
		delete p;
		p = nullptr;
		cout << "~Student()" << endl;
	}

public:
	int* p = new int[100];
};

int main()
{
	Person* pp = new Person;
	//等价于pp->destructor(),不构成多态,
	//调用函数的时候按照pp的类型调用
	delete pp;

	Person* sp = new Student;
	//等价于sp->destructor(),不构成多态,
	//调用函数的时候按照sp的类型调用
	delete sp;

	return 0;
}

在这里插入图片描述

析构函数是虚函数,并构成多态:

class Person
{
public:
	//析构函数构成多态
	virtual ~Person()
	{
		cout << "~Person()" << endl;
	}
};
class Student : public Person
{
public:
	~Student()
	{
		delete p;
		p = nullptr;
		cout << "~Student()" << endl;
	}

public:
	int* p = new int[100];
};

int main()
{
	Person* pp = new Person;
	//等价于pp->destructor(),构成多态,
	//调用函数的时候按照pp指向的对象调用
	delete pp;

	Person* sp = new Student;
	//等价于sp->destructor(),构成多态,
	//调用函数的时候按照sp的指向的对象调用
	delete sp;

	return 0;
}

在这里插入图片描述
这样就能正确地调用析构函数释放资源了。

总结:多态,就是不同对象传递给成员函数的this,调用不同的函数。如果是多态,调用函数时看指针或者引用指向的对象。如果不构成多态,调用函数时只看该指针或者引用本身的类型。

2.4 C++11中的override和final

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

1、override用来修饰子类虚函数的,用于检查子类虚函数是否对基类的某个虚函数完成了重写,如果没有,则报错。
在这里插入图片描述
2、final用于修饰虚函数,表示虚函数不能再被重写。
在这里插入图片描述

2.5 重写(覆盖),重载,重定义(隐藏)的对比

在这里插入图片描述

三、多态的原理

3.1 虚函数表

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

	return 0;
}

答案是8,为什么呢?明明只有一个整形的成员变量,不应该是4吗?因为Base类中存在虚函数,所以成员变量中增加了一个虚函数表指针,指向的虚函数表存放着所有虚函数的地址。
在这里插入图片描述
通过观察测试我们发现b对象是8字节,除了_b成员,还多一个_vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。

那么子类的虚表中又存放了什么呢?

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:
	//子类重写Func1
	virtual void Func1()
	{
		cout << "Derive::Func1()" << endl;
	}
private:
	int _d = 2;
};
int main()
{
	Base b;
	Derive d;

	return 0;
}

在这里插入图片描述

  1. 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,另一部分是自己的成员。
  2. 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法层的叫法,覆盖是原理层的叫法。
  3. 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表。
  4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。
  5. 总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

虚表存的是虚函数指针,不是虚函数,虚函数和普通函数是一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。那么虚表存在哪的呢?下面我们可以通过一段代码验证一下:
在这里插入图片描述
由上推断出虚表是存放在常量区的。

3.2 再谈多态的条件

一、父类的指针或者引用调用虚函数
问题1:为什么不能是子类的指针或者引用调用虚函数?
因为父类的指针或者引用既能够接收父类的指针或者引用,也能够接收子类的指针或者引用(切片)。
问题2:为什么不能是父类对象调用虚函数?
解释如下:
在这里插入图片描述
二、虚函数的重写
为什么构成多态一定要子类重写父类的虚函数?
因为只有完成重写,子类的虚函数表的对应的函数地址才会被覆盖成子类重写之后的虚函数的地址,在调用时才能根据父类指针指向的对象调用对应的虚函数,指向父类对象就到父类的虚函数表里找虚函数的指针并调用函数,指向子类对象就到子类虚函数表中查找虚函数的指针并调用函数,这样就能完成多态。

在这里插入图片描述

3.3 动态绑定和静态绑定

  1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载。
  2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。

3.4 单继承和多继承关系的虚函数表

3.4.1 单继承中的虚函数表

打印虚表:

class Base {
public:
	virtual void func1() { cout << "Base::func1" << endl; }
	virtual void func2() { cout << "Base::func2" << endl; }
private:
	int a;
};
class Derive :public Base {
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
	virtual void func4() { cout << "Derive::func4" << endl; }
private:
	int b;
};

//函数指针
typedef void (*FUNC_PTR) ();

void PrintVFT(FUNC_PTR table[])
{
	//虚表是以nullptr为结束标志的,所以
	// 可以借助这个来当循环的结束条件
	for (int i = 0; table[i] != nullptr; i++)
	{
		printf("[%d]->%p\n", i, table[i]);
	}
	printf("\n");
}

int main()
{
	Base b;
	Derive d;
	int ptr = *(int*)&d;
	PrintVFT((FUNC_PTR*)ptr);

	return 0;
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3.4.2 多继承中的虚函数表

打印虚函数表:

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

函数指针
typedef void (*FUNC_PTR) ();

void PrintVFT(FUNC_PTR table[])
{
	//虚表是以nullptr为结束标志的,所以
	// 可以借助这个来当循环的结束条件
	for (int i = 0; table[i] != nullptr; i++)
	{
		printf("[%d]:%p->", i, table[i]);
		FUNC_PTR func = table[i];
		func();
	}
	printf("\n");
}

int main()
{
	Derive d;
	cout << sizeof(d) << endl;

	int vft1 = *((int*)&d);
	Base2* ptr = &d;
	int vft2 = *((int*)ptr);

	PrintVFT((FUNC_PTR*)vft1);
	PrintVFT((FUNC_PTR*)vft2);

	return 0;
}

观察下图可以看出:多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中。
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

四、练习

第一题、
在这里插入图片描述

第二题、
在这里插入图片描述

第三题、
在这里插入图片描述
第四题、
在这里插入图片描述

五、思考以下问题

  1. 什么是多态?答:参考文章内容。
  2. 什么是重载、重写(覆盖)、重定义(隐藏)?答:参考文章内容。
  3. 多态的实现原理?答:参考文章内容。
  4. inline函数可以是虚函数吗?答:可以,不过编译器会自动忽略inline属性,这个函数就不再是inline函数了,因为虚函数要放到虚表中去,而inline函数是在编译时展开的,并没有函数的地址,所以虚函数地址要放到虚表中去就一定要有函数地址。
  5. 静态成员可以是虚函数吗?答:不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
  6. 构造函数可以是虚函数吗?答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。
  7. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?答:可以,并且最好把基类的析构函数定义成虚函数。参考文章内容。
  8. 对象访问普通函数快还是虚函数更快?答:如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为一旦构成多态,在运行时调用虚函数就需要到虚函数表中去查找。
  9. 虚函数表是在什么阶段生成的,存在哪的?答:虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。(参考文章验证的内容。)
  10. C++菱形继承的问题?虚继承的原理?答:参考上一篇关于继承的文章。注意不要把虚函数表和虚基表搞混了。

以上就是今天想要跟大家分享的内容了,你学会了吗?多态是非常重要的内容哦,实践中也是经常使用的,所以一定要掌握哦,如果你感觉到有所帮助的话,就点点赞点点关注呗,后期还会持续更新C++相关的知识哦,我们下期见啦!!!!!!!!!!!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/45596.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

微分流形2:流形上的矢量场和张量场

来了来了&#xff0c;切向量&#xff0c;切空间。流形上的所有的线性泛函的集合&#xff0c;注意是函数的集合。然后取流形上的某点p&#xff0c;它的切向量为&#xff0c;线性泛函到实数的映射。没错&#xff0c;是函数到实数的映射&#xff0c;是不是想到了求导。我们要逐渐熟…

基于FPGA实现OSD功能

简介 基于FPGA平台实现简单的OSD的功能,对于FPGA实现OSD只能实行简单的画框和文字叠加,如果实现复杂的车道线画框,则没法实现(起码我个人感觉,这个功能没有思路执行)。 FPGA实现OSD功能需要7系列平台,以及VDMA、OSD等Xilinx公司的IP使用(本功能工程采用Vivado2017.4平台…

OSCP最新考试QA

枚举提示 初始枚举 对你的目标进行光线扫描。 例如&#xff0c;扫描您的考试机器上的10个常见端口。 在等待彻底和更长时间的扫描时&#xff0c;手动与找到的服务交互。 仔细列举 避免对多个目标进行大量扫描。 运行不安全扫描后还原计算机。 重新运行扫描以确保所有信…

【Unity3D日常开发】Unity3D中比较string字符串的常用方法

推荐阅读 CSDN主页GitHub开源地址Unity3D插件分享简书地址我的个人博客 大家好&#xff0c;我是佛系工程师☆恬静的小魔龙☆&#xff0c;不定时更新Unity开发技巧&#xff0c;觉得有用记得一键三连哦。 一、前言 字符串string的比较有很多方法&#xff0c;比如&#xff1a; …

MongoDB原生语句更新嵌套数组的值

一、更新一层嵌套数组 首先执行MongoDB原生语句脚本在user集合中产生一些样本数据,如下所示: db.user.insert({"_id":1,"title":"爱情公寓3","students":[{"student_id":1001,"student_name":"林宛瑜&quo…

表单验证:输入的字符串以回车分隔并验证是否有

公司项目开发时&#xff0c;有一个需求&#xff0c;需要对输入的字符串按回车分隔并验证是否有重复项&#xff0c;效果如下&#xff1a; 表单代码&#xff1a; <el-form-item label"IP地址条目&#xff1a;" prop"ipAddressEntry"><el-inputtype&…

计算机内存中的缓存Cache Memories

这篇写一下计算机系统中的缓存Cache应用场景和实现方式介绍。 Memory hierarchy 在讲缓存之前&#xff0c;首先要了解计算机中的内存结构层次Memory hierarchy。也就是下图金字塔形状的结构。 从上到下&#xff0c;内存层次结构如下&#xff1a; 寄存器&#xff1a;这是计算机…

FPGA_学习_13_方差计算小模块

测距器件APD的性能与器件本身的温度、施加在APD的偏置电压息息相关。 在不同的温度下&#xff0c;APD的偏压对测距性能的影响非常大。 要确定一个合适的APD的偏压Vopt&#xff0c;首先你要知道当前温度下&#xff0c;APD的击穿电压Vbr&#xff0c;一般来讲&#xff0c;Vopt Vb…

桥梁安全生命周期监测解决方案

一、方案背景 建筑安全是人们生产、经营、居住等经济生活和人身安全的基本保证&#xff0c;目前我国越来越多的建筑物逐 步接近或者已经达到了使用年限&#xff0c;使得建筑物不断出现各种安全隐患&#xff0c;对居民的人身安全和财产安全产 生不利影响&#xff0c;因此房…

gitee 配置ssh 公钥(私钥)

步骤1&#xff1a;添加/生成SSH公钥&#xff0c;码云提供了基于SSH协议的Git服务&#xff0c;在使用SSH协议访问项目仓库之前&#xff0c;需要先配置好账户/项目的SSH公钥。 绑定账户邮箱&#xff1a; git config --global user.name "Your Name" git config --glob…

看了2023年的一线互联网公司时薪排行榜!值得思考

前言 根据最近针对国内的一线互联网企业做的调研&#xff0c;汇总了他们的平均时薪水平&#xff0c;最终出了一个排行榜&#xff01; 首先我们来看下&#xff0c;排行榜分哪几个Level&#xff0c;分别为初级、中级、高级、资深、专家/架构这五个&#xff0c;主要根据工程师的…

opencv对相机进行畸变矫正,及矫正前后的坐标对应

文章目录 1.背景2.需求分析3.解决方案3.1.镜头畸变矫正3.2.知道矫正后的画面坐标&#xff08;x&#xff0c;y&#xff09;&#xff0c;求其在原画面的坐标&#xff08;x&#xff0c;y&#xff09;3.2.知道原画面坐标&#xff08;x1&#xff0c;y1&#xff09;&#xff0c;求其在…

fastadmin框架重定向

由于&#xff0c;我们一打开fastadmin框架就进入到前端页面很麻烦&#xff0c;下面这种方法可以解决这个问题。 首先我们找到这个路径 找到重定向&#xff0c; application》index》controller》index 原本文件是这个样子&#xff1a; <?phpnamespace app\index\controll…

【ArcGIS Pro二次开发】(53):村规制表、制图【福建省】

这篇算是村规入库的一个延续。 村庄规划中有一些图纸是需要严格按照规范制图&#xff0c;或形成一定规范格式的。 这些图纸的制作基本算是机械式的工作&#xff0c;可以用工具来代替人工。 一、要实现的功能 如上图所示&#xff0c;在【村庄规划】组&#xff0c;新增了两个工…

配置代理——解决跨域问题(详解)

之前写项目的时候总会遇到配置代理的问题&#xff0c;可是配置了之后有时有用&#xff0c;有时就没有用&#xff0c;自己之前学的也是懵懵懂懂&#xff0c;于是专门花了一个小时去了解了如何配置代理跨域&#xff0c;然后在此记录一下&#xff0c;方便自己以后查阅。 一、 常用…

pytorch实现梯度下降算法例子

如题&#xff0c;利用pytorch&#xff0c;通过代码实现机器学习中的梯度下降算法&#xff0c;求解如下方程&#xff1a; f ′ ( x , y ) x 2 20 y 2 {f}(x,y) x^2 20 y^2 f′(x,y)x220y2 的最小值。 Latex语法参考&#xff1a;https://blog.csdn.net/ViatorSun/article/d…

推荐系统(十)用户行为序列建模-Pooling 路线

对推荐系统而言&#xff0c;准确捕捉用户兴趣是其面临的核心命题。不管是样本、特征还是模型结构等方面的优化&#xff0c;本质上做的事情都是在提高推荐系统对用户兴趣的捕捉能力&#xff0c;因此如何提高这种能力&#xff0c;对推荐效果的提升有重要作用&#xff0c;也是算法…

tp6 实现excel 导入功能

在项目根目录安装 composer require phpoffice/phpspreadsheet 我们看一下郊果图&#xff0c;如下 点击导入excel表格数据 出现弹窗选择文件&#xff0c;控制台打开输出文档内容 前端layui代码 <form id"uploadForm" class"form-horizontal" encty…

7.25 Qt

制作一个登陆界面 login.pro文件 QT core guigreaterThan(QT_MAJOR_VERSION, 4): QT widgetsCONFIG c11# The following define makes your compiler emit warnings if you use # any Qt feature that has been marked deprecated (the exact warnings # depend on …

如何高效地查询IP归属地

高效识别IP归属地是网络安全领域中的一项重要工作。准确地识别IP的归属地不仅可以帮助网络管理员追踪和定位潜在的网络攻击者&#xff0c;还可以用于网络流量分析、地理定位服务等方面。 以下将介绍几种高效识别IP归属地的方法。 使用IP归属地数据库 IP归属地数据库是一种存储…