第七层:多态

news2024/11/15 13:39:00

文章目录

  • 前情回顾
  • 多态
    • 多态的基本概念
    • 动态多态的满足条件
    • 动态多态的使用
    • 虚函数
    • 多态的优点
    • 纯虚函数和抽象类
      • 抽象类特点
    • 虚析构和纯虚析构
      • 虚析构和纯虚析构的共性
      • 虚析构和纯虚析构的区别
  • 面向对象结束,接下来是什么?
  • 本章知识点(图片形式)

🎉welcome🎉
✒️博主介绍:一名大一的智能制造专业学生,在学习C/C++的路上会越走越远,后面不定期更新有关C/C++语法,数据结构,算法,Linux,ue5使用,制作游戏的心得,和大家一起共同成长。
✈️C++专栏:C++爬塔日记
😘博客制作不易,👍点赞+⭐收藏+➕关注

前情回顾

在第六层中,我遇到了继承,它是面向对象三大特性之一,它也是我遇到的第二个面向对象的特性,因为继承,C++中的类被分成子类和父类,还有虚继承等强大的力量,但是,我还是掌握,走向了第七层…

  • 🚄上章地址:第六层:继承

多态

“你来了啊,这层有着面向对象的最后一种特性——多态,它是每一个C++程序员都必须掌握的核心技术,希望你也可以掌握…”“面向对象的最后一中特性了吗?看起来是一场恶战。”

多态的基本概念

多态是C++面向对象的三大特性之一,多态分为两类:

  • 静态多态:两种重载(函数重载和运算符重载)属于静态多态,复用函数名
  • 动态多态:子类和虚函数实现运行时发生的多态

那这两种多态有什么区别呢?

  • 静态多态的函数地址早绑定(在编译阶段确定函数地址)
  • 动态多态的函数地址晚绑定(运行阶段确定函数地址)

那什么是晚绑定,什么是早绑定?下面就是早绑定的案例:

  • 现在有一个函数,它的参数是父类引用,里面调用父类和子类当中都有的函数,现在传过去一个子类,可以调用吗?可以的话,是调用子类还是父类?
#include<iostream>
using namespace std;

class A
{
public:
	void a1()
	{
		cout << "A在调用" << endl;
	}
};

class A1 :public A
{
public:
	void a1()
	{
		cout << "A1在调用" << endl;
	}
};

void polym(A& a)
{
	a.a1();
}
void test1()
{
	A1 a;
	polym(a);
}
int main()
{
	test1();
	return 0;
}

在这里插入图片描述
是可以调用的,因为在C++中,允许父子之间的类型转换,不需要做强制类型转换,父类的引用可以直接指向子类,但是反省调用的是父类中的函数,那为什么传过去一个子类却调用的是父类呢?因为在底层,是调用父类,这个时候就是因为地址的早绑定,在编译阶段就确定了函数地址,所以不管传子还是父,都会指向父类中的函数地址,调用父类中的函数地址,如果要执行子类,那便不能让编译器就编译阶段就确定函数地址,不能进行早绑定,需要在运行时在进行绑定,这个时候就叫做地址晚绑定,需要在父类成员函数前加:

  • virtual

这个时候成员函数就变成了虚函数,也就实现晚绑定了:

#include<iostream>
using namespace std;

class A
{
public:
	virtual void a1()
	{
		cout << "A在调用" << endl;
	}
};

class A1 :public A
{
public:
	void a1()
	{
		cout << "A1在调用" << endl;
	}
};

void polym(A& a)
{
	a.a1();
}
void test1()
{
	A1 a;
	polym(a);
}
int main()
{
	test1();
	return 0;
}

在这里插入图片描述

动态多态的满足条件

在上面所提到的地址晚绑定,就是动态多态,那要实现动态多态,有一些需要满足的条件:

  • 有继承关系
  • 子类中要重写父类中虚函数(返回类型、函数名、参数列表都要一致),对于子类中的函数重写,virtual可加可不加

动态多态的使用

  • 使用父类指针或者引用去执行子类对象

那具体为什么会这样呢?就是因为虚函数的作用。

虚函数

在第三层:C++中的对象和this指针中提到了,在类内没有非静态成员时的大小为一,那类内只有有虚函数的时候,大小是多少?

#include<iostream>
using namespace std;

class A
{
public:
	virtual void a1()
	{
		cout << "A在调用" << endl;
	}
};

class A1 :public A
{
public:
	void a1()
	{
		cout << "A1在调用" << endl;
	}
};

void test1()
{
	cout << sizeof(A) << endl;
}
int main()
{
	test1();
	return 0;
}

在这里插入图片描述
是8,那它的本质其实是指针,因为我编译器的环境是x64,所以是8,那虚函数的内部指向的其实和虚继承一样,内部也只一个指针,但是是vfptr,叫做虚函数指针,会指向vftable,叫做虚函数表,这个虚函数表内部是记录的是虚函数地址,当父类的指针指向的是指针或者引用的时候,发生多态,当子类通过父类引用调用函数的时候,就会去子类的虚函数表内调用子类当中的函数,因为重写,子类的函数就会覆盖掉自己虚函数表中的父类函数,这个时候就会调用子类函数。
在这里插入图片描述

多态的优点

  • 代码组织结构清晰
  • 可读性强
  • 利于前期和后期的扩展以及维护

可以用代码实现一个不用多态的和一个使用多态的来对比验证:

  • 现在处于一个麦饮料的地方,需要你选择饮料

普通方法:

#include<string>
#include<iostream>
using namespace std;

class drink
{
public:
	void dele(string s)
	{
		if (s == "mike")
		{
			cout << "牛奶来咯" << endl;
		}
		else if (s == "orange")
		{
			cout << "橙汁来咯" << endl;
		}
		else if (s == "coke")
		{
			cout << "可乐来咯" << endl;
		}
	}	
	string _d;
};
void test1()
{
	drink d;
	cin >> d._d;
	d.dele(d._d);
}
int main()
{
	test1();
	return 0;
}

在这里插入图片描述
多态实现:

#include<string>
#include<iostream>
using namespace std;

class drink
{
public:
	virtual void dele()
	{
		
	}	
};
class mike :public drink
{
public:
	void dele()
	{
		cout << "牛奶来咯" << endl;
	}
};
class orange :public drink
{
public:
	void dele()
	{
		cout << "橙汁来咯" << endl;
	}
};
class coke :public drink
{
public:
	void dele()
	{
		cout << "可乐来咯" << endl;
	}
};
void test1()
{
	drink *d = new coke;
	d->dele();
	delete d;
	d=NULL;
}
int main()
{
	test1();
	return 0;
}

在这里插入图片描述
可以发现,多态的代码量要远远超于普通的写法,那为什么还要使用多态?就是因为多态的三个优点,并且,对于第三个优点来说,对于普通写法,要加入什么饮品,需要修改原码,而在真实的开发中,是不建议去修改的,提倡开闭原则1

纯虚函数和抽象类

上面例子中,可以看到对于父类中的虚函数是基本不会调用的,用的多的是子类中的同名函数,这个时候,可以将父类中的虚函数变成纯虚函数,语法:

  • virtual 返回类型 函数名 (参数) =0;(大括号不用写)

这个时候,当类内有了纯虚函数,这个类也就被称为抽象类。

抽象类特点

  • 抽象类无法实例化对象
  • 子类必须重写抽象类函数中的纯虚函数,否则子类也会成为抽象类

验证抽象类无法实例化对象:

#include<string>
#include<iostream>
using namespace std;

class drink
{
public:
	virtual void dele() = 0;
};
class mike :public drink
{
public:
	void dele()
	{
		cout << "牛奶来咯" << endl;
	}
};
class orange :public drink
{
public:
	void dele()
	{
		cout << "橙汁来咯" << endl;
	}
};
class coke :public drink
{
public:
	void dele()
	{
		cout << "可乐来咯" << endl;
	}
};
void test1()
{
	drink d;
}
int main()
{
	test1();
	return 0;
}

在这里插入图片描述
验证子类不重写函数也将变成抽象类:

#include<string>
#include<iostream>
using namespace std;

class drink
{
public:
	virtual void dele() = 0;
};
class mike :public drink
{
public:
	void dele(int a)
	{
		cout << "牛奶来咯" << endl;
	}
};
class orange :public drink
{
public:
	void dele()
	{
		cout << "橙汁来咯" << endl;
	}
};
class coke :public drink
{
public:
	void dele()
	{
		cout << "可乐来咯" << endl;
	}
};
void test1()
{
	mike d;
}
int main()
{
	test1();
	return 0;
}

在这里插入图片描述

虚析构和纯虚析构

  • 在使用多态的时候,子类中有成员属性开辟空间到堆区,则父类指针在释放的时候无法调用子类中的析构函数

验证:

#include<string>
#include<iostream>
using namespace std;

//人
class people
{
public:
	people()//查看是否调用
	{
		cout << "父类内构造函数调用" << endl;
	}
	~people()//查看是否调用
	{
		cout << "父类内析构函数调用" << endl;
	}
	virtual void come() = 0;//纯虚函数
};
//男人
class man :public people
{
public:
	man(string _name)//查看是否调用
	{
		cout << "子类内构造函数调用"	<< endl;
		this->_name = new string(_name);//对名字进行初始化
	}
	~man()//查看是否调用
	{
		if (_name != NULL)
		{
			cout << "子类内析构函数调用" << endl;
			delete _name;
			_name = NULL;
		}
	}
	void come()
	{
		cout <<*_name<<"来咯" << endl;
	}
	string* _name;//用指针来管理名字
};

void test1()
{
	people* p = new man("张三");
	p->come();
	delete p;
	p = NULL;
}
int main()
{
	test1();
	return 0;
}

在这里插入图片描述
那这个时候就会造成内存泄漏,那怎么去解决呢?

  • 将父类中的析构函数变成虚析构或者纯虚析构
#include<string>
#include<iostream>
using namespace std;

//人
class people
{
public:
	people()//查看是否调用
	{
		cout << "父类内构造函数调用" << endl;
	}
	virtual ~people()//查看是否调用
	{
		cout << "父类内析构函数调用" << endl;
	}
	virtual void come() = 0;//纯虚函数
};
//男人
class man :public people
{
public:
	man(string _name)//查看是否调用
	{
		cout << "子类内构造函数调用"	<< endl;
		this->_name = new string(_name);//对名字进行初始化
	}
	~man()//查看是否调用
	{
		if (_name != NULL)
		{
			cout << "子类内析构函数调用" << endl;
			delete _name;
			_name = NULL;
		}
	}
	void come()
	{
		cout <<*_name<<"来咯" << endl;
	}
	string* _name;//用指针来管理名字
};

void test1()
{
	people* p = new man("张三");
	p->come();
	delete p;
	p = NULL;
}
int main()
{
	test1();
	return 0;
}

在这里插入图片描述
选择就会走子类的调用,如果是纯虚析构呢?

#include<string>
#include<iostream>
using namespace std;

//人
class people
{
public:
	people()//查看是否调用
	{
		cout << "父类内构造函数调用" << endl;
	}
	virtual ~people() = 0;//查看是否调用
	virtual void come() = 0;//纯虚函数
};
//男人
class man :public people
{
public:
	man(string _name)//查看是否调用
	{
		cout << "子类内构造函数调用"	<< endl;
		this->_name = new string(_name);//对名字进行初始化
	}
	~man()//查看是否调用
	{
		if (_name != NULL)
		{
			cout << "子类内析构函数调用" << endl;
			delete _name;
			_name = NULL;
		}
	}
	void come()
	{
		cout <<*_name<<"来咯" << endl;
	}
	string* _name;//用指针来管理名字
};

void test1()
{
	people* p = new man("张三");
	p->come();
	delete p;
	p = NULL;
}
int main()
{
	test1();
	return 0;
}

在这里插入图片描述
那这里是为什么呢?可以看到报错内容说纯虚析构是无法解析的外部符号,对比上面的虚析构,发现,纯虚析构没有定义,纯虚函数可以不用定义,那为什么纯虚析构就需要定义了呢?这是因为,析构是所有类在销毁前会走的一个函数,这个时候,内部没有实现,就走不过去,就会产生报错,那解决方法就是,在类外定义析构函数,需要在析构函数名前加上作用域:

#include<string>
#include<iostream>
using namespace std;

//人
class people
{
public:
	people()//查看是否调用
	{
		cout << "父类内构造函数调用" << endl;
	}
	virtual ~people() = 0;//查看是否调用
	virtual void come() = 0;//纯虚函数
};
people::~people()
{
		cout << "父类内析构函数调用" << endl;
}
//男人
class man :public people
{
public:
	man(string _name)//查看是否调用
	{
		cout << "子类内构造函数调用"	<< endl;
		this->_name = new string(_name);//对名字进行初始化
	}
	~man()//查看是否调用
	{
		if (_name != NULL)
		{
			cout << "子类内析构函数调用" << endl;
			delete _name;
			_name = NULL;
		}
	}
	void come()
	{
		cout <<*_name<<"来咯" << endl;
	}
	string* _name;//用指针来管理名字
};

void test1()
{
	people* p = new man("张三");
	p->come();
	delete p;
	p = NULL;
}
int main()
{
	test1();
	return 0;
}

在这里插入图片描述

虚析构和纯虚析构的共性

  • 可以解决父类指针释放子类对象
  • 都需要有具体的函数实现

虚析构和纯虚析构的区别

  • 有纯虚析构的类也是抽象类,无法实例化对象

验证:

#include<string>
#include<iostream>
using namespace std;

//人
class people
{
public:
	people()//查看是否调用
	{
		cout << "父类内构造函数调用" << endl;
	}
	virtual ~people() = 0;//查看是否调用
	virtual void come() = 0;//纯虚函数
};
people::~people()
{
		cout << "父类内析构函数调用" << endl;
}
//男人
class man :public people
{
public:
	man(string _name)//查看是否调用
	{
		cout << "子类内构造函数调用"	<< endl;
		this->_name = new string(_name);//对名字进行初始化
	}
	~man()//查看是否调用
	{
		if (_name != NULL)
		{
			cout << "子类内析构函数调用" << endl;
			delete _name;
			_name = NULL;
		}
	}
	void come()
	{
		cout <<*_name<<"来咯" << endl;
	}
	string* _name;//用指针来管理名字
};

void test1()
{
	people p;
}
int main()
{
	test1();
	return 0;
}

在这里插入图片描述

面向对象结束,接下来是什么?

随着石碑倒下,看着眼前的楼梯,我心情沉重,“面向对象结束了,那接下来会是什么?”

本章知识点(图片形式)

在这里插入图片描述

😘预知后事如何,关注新专栏,和我一起征服C++这座巨塔
🚀专栏:C++爬塔日记
🙉都看到这里了,留下你们的👍点赞+⭐收藏+📋评论吧🙉


  1. 开闭原则:对扩展进行开发,对修改进行关闭。 ↩︎

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

数据结构进阶 哈希桶

作者&#xff1a;小萌新 专栏&#xff1a;数据结构进阶 作者简介&#xff1a;大二学生 希望能和大家一起进步&#xff01; 本篇博客简介&#xff1a;模拟实现高阶数据结构 哈希桶 哈希桶哈希冲突的另一种解决方法开散列 -- 链地址法举例哈希表的开散列实现 --哈希桶哈希表的结构…

自动化测试Selenium【基础篇二】

自动化测试Selenium【基础篇二】&#x1f34e;一.Selenium基础使用&#x1f352;1.1 信息打印&#x1f349; 1.1.1打印标题&#x1f349; 1.1.1打印当前网页标题&#x1f352;1.2 窗口&#x1f349;1.2.1 获取句柄&#x1f349;1.2.2 窗口切换&#x1f349;1.2.3 窗口大小设置&…

当你点击浏览器的瞬间都发生了什么----- 网络学习笔记

计算机网络前言web 浏览器协议栈创建套接字阶段。连接阶段。断开阶段。IP模块网卡网络设备 --- 集线器、交换器和路由器集线器交换器路由器路由器的附加功能一 &#xff1a;地址转换路由器的附加功能一 &#xff1a;包过滤功能互联网内部接入网光纤接入网&#xff08;FTTH&…

JDK8 前后的 Date 日期时间 API

JDK8 前后的 Date 日期时间 API 每博一文案 师父说&#xff1a;人只要活在世界上&#xff0c;就会有很多的烦恼&#xff0c;痛苦或是快乐&#xff0c;取决于逆的内心&#xff0c;只要心里拥有温暖灿烂的阳光&#xff0c; 那么悲伤又有什么好畏惧的呢&#xff1f; 人生如行路&a…

vue学习笔记(更新中)

目录 简介 使用Vue写一个"hello&#xff0c;world" 前置准备 代码书写 MVVM模型理解 插值语法和指令语法 插值语法 指令语法 指令&#xff1a;v-bind 指令&#xff1a;v-model vue中的el和data的两种写法 数据代理 方法&#xff1a;defineProperty() 说明…

新年礼物已收到!2022 Apache IoTDB Commits 数量排名 3/364!

社区喜报&#xff01;据 The Apache Software Foundation 官方 Projects Statistics&#xff08;项目信息统计网站&#xff09;的实时数据显示&#xff0c;Apache IoTDB 在过去 12 个月&#xff08;即 2022 年度&#xff09;共发表 6829 Commits&#xff0c;排名 2022 年度 Apa…

2、Three.js开发入门与调试设置

一、添加坐标轴辅助器 AxesHelper 用于简单模拟3个坐标轴的对象. 红色代表 X 轴. 绿色代表 Y 轴. 蓝色代表 Z 轴. 构造函数 AxesHelper( size : Number ) size -- (可选的) 表示代表轴的线段长度. 默认为 1. //添加坐标轴 const axesHelper new THREE.AxesHelper(5); sc…

CSS 特效之心形-彩虹-加载动画

CSS 特效之心形-彩虹-加载动画参考描述效果HTMLCSS重置元素的部分默认样式bodyli动画定义指定animationul抖动代码总汇参考 项目描述搜索引擎BingMDNMDN Web Docs 描述 项目描述Edge109.0.1518.61 (正式版本) (64 位) 效果 HTML <!DOCTYPE html> <html lang"e…

Keil C51工程转VSCode Keil Assistant开发全过程

Keil C51工程转VSCode Keil Assistant开发全过程✨这里以stc15W408AS为例。&#x1f4cc;相关篇《【开源分享】自制STC15W408AS开发板》 &#x1f4fa;编译-烧录演示&#xff1a; &#x1f4cb;转VSCODE开发环境主要原因可能代码提示以及代码跳转功能&#xff0c;或者其他。 &…

在java中操作redis

在普通项目中操作redis 1.导入maven坐标 <dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId><version>2.8.0</version> </dependency>2.打开redis 如果redis-server闪退&#xff0c;那就先打开re…

【Spring Security】如何使用Lambda DSL配置Spring Security

本期目录1. 概述2. 新老配置风格对比Lambda风格等效的旧配置风格3. WebFlux Security4. Lambda DSL的目标1. 概述 在 Spring Security 5.2 中增强了 DSL 的功能&#xff1a;允许使用 Lambda 表达式来配置 HTTP security 。 需要注意的是&#xff1a;先前版本的配置风格仍然是…

二分查找——“C”

各位CSDN的uu们你们好呀&#xff0c;欢迎来到小雅兰的课堂&#xff0c;今天我们的内容是复习之前的内容&#xff0c;并把之前的内容的一些习题一起来做一做&#xff0c;现在&#xff0c;就让我们进入二分查找的世界吧 首先&#xff0c;我们介绍的题目就是二分查找&#xff0c;也…

module ‘tensorflow‘ has no attribute ‘Session‘

1. module ‘tensorflow‘ has no attribute ‘Session‘ 指定一个会话来运行tensorflow程序&#xff0c;在使用tensorflow1.x版本中用tensorflow.Session即可 但当我的库版本升级到2.x之后&#xff0c;就会出现标题式报错&#xff0c;于是我去查看了tensorflow库的源码&…

使用Keras搭建深度学习模型

前言 目前深度学习领域的主流框架 tensorflowkeraspytorchcaffetheanopaddlepaddle keras 代码架构 keras代码风格相比于其他框架更符合人的思维。 模型 模型的组成分为三部分&#xff1a;输入层、网络层、输出层。 输入层 输入层的作用时规定了模型输入的shape fro…

2022年智源社区年度热点推荐丨新春集锦

本文为2022年最受智源社区小伙伴喜爱的文章&#xff0c;根据文章质量和热门程度等维度计算得出。还有AI大佬的全年总结盘点总结&#xff0c;也一并推荐给你。虎年除旧&#xff0c;兔年迎新&#xff0c;藉此机会、智源编辑组全员谨祝大家新春快乐&#xff01;2022智源社区20篇最…

LINUX学习之网络配置(十一)

1.修改IP地址 使用ifconfig命令 例如要将eth0接口的IP地址更改为192.168.1.100&#xff0c;你可以使用以下命令 ifconfig eth0 192.168.1.100如果你想为IP地址设置子网掩码&#xff0c;可以使用“netmask”参数。例如&#xff0c;要将eth0接口的子网掩码设置为255.255.255.0…

[Linux]进程优先级 Linux中的环境变量

&#x1f941;作者&#xff1a; 华丞臧. &#x1f4d5;​​​​专栏&#xff1a;【LINUX】 各位读者老爷如果觉得博主写的不错&#xff0c;请诸位多多支持(点赞收藏关注)。如果有错误的地方&#xff0c;欢迎在评论区指出。 推荐一款刷题网站 &#x1f449; LeetCode刷题网站 文…

liunx centos9中安装Redis数据库,并在win10中连接redis图文详解

首先我们去Redis的官网点击download下载tar的压缩包 https://redis.io/download/#redis-downloads 用xftp将安装包上传到你的liunx服务器本地地址 解压 tar -xvf /root/redis-7.0.8.tar.gz cd进入你刚才解压的文件夹中 cd /root/redis-7.0.8 执行make进行编译 编译完成后cd进入…

普中科技MicroPython基于esp32的基础教程-03-字符串

目录 字符串 字符串的表示方式 普通字符串 原始字符串 长字符串 字符串与数字相互转换 将字符串转换为数字 将数字转换为字符串 格式化字符串 占位符% format方法 f-strings 操作字符串 字符串拼接 字符串查找 字符串替换 字符串分割 去处字符串两…

目标检测:Focal Loss

目标检测&#xff1a;Focal Loss前言Focal LossCross Entropybalanced Cross EntropyFocal Loss Definition前言 Focal loss这个idea来源于论文《Focal Loss for Dense Object Detection》,主要是为了解决正负样本、难易样本不平衡的问题。 Focal Loss Cross Entropy 在目标…