第七层:多态

news/2024/11/20 15:29: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.ppmy.cn/news/18765.html

相关文章

51单片机数码管显示

文章目录前言一、数码管简介二、数码管原理图三、数码管显示原理四、静态数码管代表编写五、动态数码管总结前言 这篇文章将介绍数码管的显示其中包含了动态数码管和静态数码管两种。 一、数码管简介 数码管其实就是由多个发光二极管封装在一起组成“8”字型的器件当分别点亮…

SpringCloud+Ribbon 报错:java.net.unknownhostexception:XXX

SpringCloudRibbon 报错&#xff1a;java.net.unknownhostexception:XXX 问题分析&#xff1a; 网上很多的说法是依赖冲突导致&#xff0c;原因是什么呢&#xff1a;如果你的org.springframework.cloud:spring-cloud-starter-netflix-eureka-client 依赖中包含了ribbon依赖&…

学习记录667@项目管理之项目人力资源管理

什么是项目人力资源管理 项目人力资源管理包括编制人力资源管理计划、组建项目团队、建设项目团队与管理项目团队的各个过程&#xff0c;不但要求充分发挥参与项目的个人的作用&#xff0c;还包括充分发挥所有与项目有关的人员-----项目负责人、客户、为项目做出贡献的个人及其…

Linux下进程以及相关概念理解

目录 一、进程概念 二、描述进程PCB 三、查看进程 3.1 通过系统目录查看 3.2 通过ps命令查看 四、进程状态 运行状态R 睡眠状态S 磁盘休眠状态D 暂停状态T 僵尸状态Z 死亡状态X 五、僵尸进程与孤儿进程 5.1 僵尸进程 5.1.1 僵尸进程的概念 5.1.2 僵尸进程的危害…

PowerShell 执行策略

在使用 SAPIEN 的PowerShell Studio时出现如下错误&#xff1a;无法在当前系统上运行该脚本。有关运行脚本和设置执行策略的详细信息&#xff0c;请参阅 https:/go.microsoft.com/fwlink/?LinkID135170 中的 about_Execution_Policies。 ERROR: 所在位置 行:1 字符: 2 ERROR: …

34.Isaac教程--操作示例应用程序

操作示例应用程序 ISAAC教程合集地址文章目录操作示例应用程序与 Jupyter Notebook 的简单联合控制Shuffle Box with Simulator与 Jupyter Notebook 的简单联合控制 此示例使用 Jupyter Notebook 提供交互式联合控制。 这是处理用于操作组件&#xff08;包括 LQR 规划器&#…

文件没学懂没关系,我来教你快速学会文件

1. 什么是文件 文件通常是在磁盘或固态上的一段已经命名的存储区。C把文件看作是一系列连续的字节&#xff0c;每个字节都被单独读取。 在程序设计中&#xff0c;我们一般谈的文件有两种&#xff1a;程序文件、数据文件&#xff08;从文件功能的角度来分类的&#xff09; 1.…

【MySQL】第九部分 MySQL信息函数

【MySQL】第九部分 MySQL信息函数 文章目录【MySQL】第九部分 MySQL信息函数9. MySQL信息函数总结9. MySQL信息函数 MySQL中内置了一些可以查询MySQL信息的函数&#xff0c;这些函数主要用于帮助数据库开发或运维人员更好地对数据库进行维护工作。 函数用法VERSION()返回当前…