【C++】C++的多态

目录

多态的使用

多态的概念

多态的定义和实现

虚函数 

构成多态的条件

特殊情况:协变

析构函数的重写

 怎么实现

 为什么实现 

override和final关键字

 override

 final 

重载/重写/隐藏的对比

纯虚函数和抽象类 

 纯虚函数

 抽象类

多态的实现 

 虚函数表指针

 多态的实现

 实现方式

 动态绑定与静态绑定

 虚函数表的存储地址


多态的使用

多态的概念

         多态从字面意思来看就是多种状态,比如说我们前面使用的函数模版swap,重载函数就是多态的一种体现只不过是在编译时就决定的是静态的,我们今天要了解的是多态的动态形式,比如我们在12306购买车票时,作为成人是原价票,学生可以享受优惠,军人可以买到提前票!

多态的定义和实现 

虚函数 

         在前面继承方面我们可能了解到了虚继承,那么什么是虚函数呢?虚函数和虚继承一样需要使用到virtual关键字,虚函数指的是被virtual修饰的成员函数,并且虚函数的出现是为了多态!

         虚函数在类域内外声明和实现时,只需要在类域声明处加上即可!

注意: 静态变量static和virtual不可以同时修饰成员函数

class Base
{
public:// 虚函数virtual void Func(){//...}
};

构成多态的条件

  •  需要存在继承关系,如基类和派生类
  •  被调用的成员函数需要是虚函数,并且构成重写/覆盖关系
  •  必须是基类的指针或者引用指向派生类对象进行调用函数

就拿我们12306买票来收,Person类和Student类,这两者是继承关系,我们需要他们的买票函数构成重写关系,重写关系就是在原本是隐藏关系的基础上给成员函数加上virtual关键字! 

// 买票举例
class Person
{
public:virtual void Buy_Ticket(){cout << "买票-原价" << endl;}
};class Student : public Person
{
public:virtual void Buy_Ticket(){cout << "买票-打折" << endl;}
};

注意:在重写基类虚函数时,派⽣类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派⽣类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用!

// 买票举例
class Person
{
public:virtual void Buy_Ticket(){cout << "买票-原价" << endl;}
};class Student : public Person
{
public:void Buy_Ticket(){cout << "买票-打折" << endl;}
};

         那么最后我们需要满足最后的第三点,使用基类的指针和引用指向派生类对象,由于继承的规定,只有基类的指针和引用可以指向派生类对象,基类的对象可以赋值派生类对象(切片的原因),为了实现多态效果我们必须使用基类的指针或者引用指向派生类对象进行调用函数!

// 买票举例
//写法一:
class Person
{
public:virtual void Buy_Ticket(){cout << "买票-原价" << endl;}void Test(){Buy_Ticket();}
};class Student : public Person
{
public:virtual void Buy_Ticket(){cout << "买票-打折" << endl;}
};int main()
{Person* p = new Student;p->Test();delete p;return 0;
}

         这段代码由于继承关系,Student继承了Person的两个函数,其中Buy_Ticket()函数构成多态被重写了,但是Test()并没有,该函数在调用时this指针是Person*的类型,我们传入的对象是Student刚好构成多态的条件!

// 写法二:
class Person
{
public:virtual void Buy_Ticket(){cout << "买票-原价" << endl;}
};class Student : public Person
{
public:virtual void Buy_Ticket(){cout << "买票-打折" << endl;}
};void Test(Person* p)
{p->Buy_Ticket();
}int main()
{Person* p = new Student;Test(p);delete p;return 0;
}

        第二种写法同样构成的多态的第三个条件,但是我们需要注意只有在多态的情况下,Test函数才会去掉用Student:: Buy_Ticket(),如果没有构成多态,那么函数Test函数调用的内容由参数类型决定!也就是Person类型。

说明:要实现多态效果,第⼀必须是基类的指针或引⽤,因为只有基类的指针或引⽤才能既指向派⽣类对象;第⼆派⽣类必须对基类的虚函数重写/覆盖,重写或者覆盖了,派⽣类才能有不同的函数,多态的不同形态效果才能达到。 

特殊情况:协变

         派⽣类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引⽤,派⽣类虚函数返回派⽣类对象的指针或者引⽤时,称为协变。

 

析构函数的重写

 怎么实现

         基类的析构函数为虚函数,此时派⽣类析构函数只要定义,⽆论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派⽣类析构函数名字不同看起来不符合重写的规则,实际上编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统⼀处理成destructor,所以基类的析构函数加了vialtual修饰,派⽣类的析构函数就构成重写。

 为什么实现 

 为什么基类中的析构函数建议设计成虚函数

防止内存泄漏:

如果不设计成虚函数 

class A
{
public:~A(){cout << "~A()" << endl;}
};class B : public A
{
public:~B(){cout << "~B()->delete" << _p << endl;}
protected:int* _p = new int[10];
};int main()
{A* p1 = new A;A* p2 = new B;delete p1;delete p2;return 0;
}

我们可以看到并没有释放B类中申请的内存! 

class A
{
public:virtual ~A(){cout << "~A()" << endl;}
};class B : public A
{
public:virtual ~B(){cout << "~B()->delete" << _p << endl;}
protected:int* _p = new int[10];
};int main()
{A* p1 = new A;A* p2 = new B;delete p1;delete p2;return 0;
}

override和final关键字

 override
  •  这个关键字和assert的作用差不多,但是override是在编译时检查,assert是在运行时检查
  •  override可以检查用户是否重写成功虚函数,如果重写失败,编译时就会报错 
 final 
  •  我们不希望派生类去重写基类的某个虚函数时,可以加上这个关键字,这样就无法重写了

 

重载/重写/隐藏的对比

 

纯虚函数和抽象类 

 纯虚函数
  •  在虚函数的后⾯写上=0,则这个函数为纯虚函数
  • 纯虚函数只需要声明即可,不用定义。注意:纯虚函数是可以定义的,只是没有必要 
// 纯虚函数和抽象类
class Pumping_paper
{
public:virtual void using_feel() = 0;void Test(){using_feel();}
};class ManHua : public Pumping_paper
{
public:void using_feel() {cout << "蓬松" << endl;}
};class QingFeng : public Pumping_paper
{
public:void using_feel() {cout << "柔软" << endl;}
};int main()
{Pumping_paper* p1 = new ManHua;Pumping_paper* p2 = new QingFeng;p1->Test();p2->Test();delete p1;delete p2;return 0;
}

 抽象类
  •  只要类中存在纯虚函数都是抽象类
  • 抽象类不能实例化出对象,如果派⽣类继承后不重写纯虚函数,那么派⽣类也是抽象类。纯虚函数某种程度上强制了派⽣类重写虚函数,因为不重写实例化不出对象。 

多态的实现 

 虚函数表指针

  • 虚函数表是一个数组

  • 这是数组存放的是指针

  • 指针是函数指针

  • 总结:存放虚函数指针的数组

/*下⾯编译为32位程序的运⾏结果是什么()
A.编译报错  B.运⾏报错  C.8  D.12*/
class A
{
public:virtual void Func(){//...}
protected:int _a = 1;char _b;
};int main()
{A a1;cout << sizeof(a1) << endl;return 0;
}

 

 

只要class存在虚函数,这些函数的指针都会存放在虚函数表中 

  • 相同类生成的多个对象共用同一种虚函数表 

 多态的实现

 实现方式
class Person
{
public:virtual void Print(){cout << "全价" << endl;}virtual void Func(){//...}void Test(){Print();}protected:string _name = "欧阳";
};class Student : public Person
{
public:virtual void Print(){cout << "打折" << endl;}virtual void Func_S(){//...}
protected:int _id = 1;
};class Child : public Person
{
public:virtual void Print(){cout << "免费" << endl;}
protected:int _age = 6;
};int main()
{Person* p1 = new Person;Person* p2 = new Student;Person* p3= new Child;p1->Test();p2->Test();p3->Test();delete p1;delete p2;return 0;
}

        类中虚函数的指针都会被存放在虚函数表中,派生类会额外开辟一块空间拷贝基类的虚函数表,然后将可以重写的虚函数地址更换为派生类的虚函数 

 动态绑定与静态绑定
  •          对不满⾜多态条件(指针或者引⽤+调⽤虚函数)的函数调⽤是在编译时绑定,也就是编译时确定调⽤函数的地址,叫做静态绑定。
  •         满⾜多态条件的函数调⽤是在运⾏时绑定,也就是在运⾏时到指向对象的虚函数表中找到调⽤函数的地址,也就做动态绑定。

静态绑定 

 动态绑定 

 

 虚函数表的存储地址

虚函数和普通函数一样存放在栈区域 

class Base {
public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }void func5() { cout << "Base::func5" << endl; }
protected:int a = 1;
};class Derive : public Base
{
public :// 重写基类的func1virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func1" << endl; }void func4() { cout << "Derive::func4" << endl; }
protected:int b = 2;
};int main()
{int i = 0;static int j = 1;int* p1 = new int;const char* p2 = "xxxxxxxx";printf("栈:%p\n", &i);printf("静态区:%p\n", &j);printf("堆:%p\n", p1);printf("常量区:%p\n", p2);Base b;Derive d;Base* p3 = &b;Derive* p4 = &d;printf("Person虚表地址:%p\n", *(int*)p3);printf("Student虚表地址:%p\n", *(int*)p4);printf("虚函数地址:%p\n", &Base::func1);printf("普通函数地址:%p\n", &Base::func5);return 0;
}

可以看出虚函数表在vs2022是被编译器存放在常量区中 !

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

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

相关文章

【C++】vector详解,模拟实现

目录 1. vector的介绍 2. vector的使用 2.1 构造函数 2.2 遍历方式 2.3 reserve与resize 2.4 shrink_to_fit 2.5 insert&#xff0c;erase&#xff0c;find 3. vector模拟实现 3.1 初始结构 3.2 析构函数 3.3 获取容量和元素个数 3.4 扩容reserve 3.5 resize改变…

方法引用(Java)

把已经有的方法拿过来用&#xff0c;当做函数式接口中抽象方法的方法体 1.引用处必须是函数式接口 2.被引用的方法必须已经存在 3.被引用的方法形参的返回值需要跟抽象方法保持一致 4.被引用方法的功能要满足当前需求 package function;import java.util.Arrays;public cl…

C++基础(3)——类和对象(中)

目录 1.类的默认成员函数 ​编辑 2. 构造函数 3. 析构函数 4. 拷⻉构造函数 5. 赋值运算符重载 5.1 运算符重载 5.2 赋值运算符重载 5.3 ⽇期类实现 6. 取地址运算符重载 6.1 const成员函数 6.2 取地址运算符重载 1.类的默认成员函数 简介&#xff1a;默认成员函数就…

day21JS-axios数据通信

1. 什么是axios axios 是一个基于Promise 用于浏览器和 nodejs 的 HTTP 客户端&#xff0c;简单的理解就是ajax的封装&#xff0c;只不过它是Promise的实现版本。 特性&#xff1a; 从浏览器中创建 XMLHttpRequests从 node.js 创建 http 请求支持 Promise API拦截请求和响应转…

论文笔记:交替单模态适应的多模态表征学习

整理了CVPR2024 Multimodal Representation Learning by Alternating Unimodal Adaptation&#xff09;论文的阅读笔记 背景MLA框架实验Q1 与之前的方法相比&#xff0c;MLA能否克服模态懒惰并提高多模态学习性能?Q2 MLA在面临模式缺失的挑战时表现如何?Q3 所有模块是否可以有…

ThreadX源码:Cortex-A7的tx_thread_irq_nesting_end(嵌套中断结束动作).s汇编代码分析

0 参考资料 Cortex M3权威指南(中文).pdf&#xff08;可以参考ARM指令集用法&#xff09; 1 前言 tx_thread_irq_nesting_end.S是用来实现Cortex-A7 IRQ嵌套中断的结束函数实现的汇编文件。 2 源码分析 源码如下&#xff1a; 1.#ifdef TX_ENABLE_FIQ_SUPPORT 2.DISABLE_INT…

【 ACM独立出版,见刊后1个月检索!!!】第二届通信网络与机器学习国际学术会议(CNML 2024,10月25-27)

第二届通信网络与机器学习国际学术会议&#xff08;CNML 2024&#xff09; The 2nd International Conference on Communication Networks and Machine Learning 官方信息 会议官网&#xff1a;www.cn-ml.org The 2nd International Conference on Communication Networks an…

jd 京东h5st 最新版 分析

声明: 本文章中所有内容仅供学习交流使用&#xff0c;不用于其他任何目的&#xff0c;抓包内容、敏感网址、数据接口等均已做脱敏处理&#xff0c;严禁用于商业用途和非法用途&#xff0c;否则由此产生的一切后果均与作者无关&#xff01; 有相关问题请第一时间头像私信联系我…

【Qt网络编程】Tcp多线程并发服务器和客户端通信

目录 一、编写思路 1、服务器 &#xff08;1&#xff09;总体思路widget.c&#xff08;主线程&#xff09; &#xff08;2&#xff09;详细流程widget.c&#xff08;主线程&#xff09; &#xff08;1&#xff09;总体思路chat_thread.c&#xff08;处理聊天逻辑线程&…

运筹说 第125期 | 存储论经典例题讲解1

通过前几期的学习&#xff0c;我们已经学会了存储论的基本概念、确定型存储模型、单周期的随机型存储模型、其他的随机型存储模型以及存储论应用研究中的一些问题。在实际工作中&#xff0c;我们能发现存储论在能源行业中有着许多应用&#xff0c;本期小编选择了其中一些确定型…

PyQt5-折叠面板效果

效果预览 实际效果中带有白色面板,看如下代码 实现代码 import sys from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout, QPushButton, QFrame, QLabel, QSizePolicy from PyQt5.QtCore import QPropertyAnimation, QEasingCurve, Qtclass CollapsiblePanel(QW…

C#:强大编程语言的多面魅力

C#&#xff1a;强大编程语言的多面魅力 一、C# 语言的特点与优势 &#xff08;一&#xff09;简洁的语法与精心设计 C# 在继承 C 和 C 的强大功能的同时&#xff0c;去掉了一些复杂特性&#xff0c;如宏和多重继承&#xff0c;使得语言更加简洁易懂。C# 是一种面向对象的语言…

openGauss之NestedLoop Join内表 Reuse

一. 前言 openGuass支持在做nestloop的时候&#xff0c;支持通过Materialize的方式将内表缓存到内存中&#xff0c;然后外表的数据内表数据进行碰撞的时候&#xff0c;如果内表已经缓存了数据&#xff0c;那么直接从缓存中直接读取内表的数据&#xff0c;从而实现内部数据Reuse…

基于SSM的在线家用电器销售系统

作者&#xff1a;计算机学姐 开发技术&#xff1a;SpringBoot、SSM、Vue、MySQL、JSP、ElementUI、Python、小程序等&#xff0c;“文末源码”。 专栏推荐&#xff1a;前后端分离项目源码、SpringBoot项目源码、SSM项目源码 系统展示 【2025最新】基于JavaSSMVueMySQL的在线家…

7--SpringBoot-后端开发、原理

配置优先级 SpringBoot 项目当中支持的三类配置文件&#xff1a; application.properties application.yml application.yaml 配置文件优先级排名&#xff08;从高到低&#xff09;&#xff1a; 1. properties配置文件 2. yml配置文件 3. yaml配置文件 在SpringBoot项目当…

MySQL 日志篇:Redo 相关线程

在 MySQL 中&#xff0c;用户线程开启事务更改数据时&#xff0c;系统内部会生成相应的 Redo Record。为了保证事务的持久性&#xff0c;这些 Redo Record 需要以 Redo Log 的形式在事务提交之前写入磁盘 (也称为“落盘”)。 为了提高事务的吞吐率 (单位时间内系统处理的事务数…

JavaSE - 面向对象编程01

01 什么是面向对象编程(oop) 答&#xff1a;就是只关心对象之间的交互&#xff0c;而并不关心任务是怎样具体完成的。例如把一个大象放进冰箱需要几步&#xff1f;如果是面向对象编程只会思考冰箱和大象之间的交互&#xff0c;那么给出的答案就是&#xff1a;把冰箱门打开&…

不可错过的AIGC浪潮:提升效率与竞争力的必备神器

随着人工智能生成内容&#xff08;AIGC&#xff09;技术的迅猛发展&#xff0c;它在提升工作效率和改善生活质量方面展示了巨大的潜力。对职场人来说&#xff0c;了解AIGC如何改变各个行业&#xff0c;并探讨其未来发展中的风险和机遇&#xff0c;将有助于他们更好地利用这项技…

三相可控整流电路 (三相半波,三相桥式)

目录 1. 三相半波整流电路 2. 三相桥式全控整流电路 三相可控整流电路利用三相交流电源&#xff0c;通过可控硅&#xff08;晶闸管&#xff09;将交流电整流为直流电。主要有两种常见类型&#xff1a;三相半波整流电路和三相桥式全控整流电路。 1. 三相半波整流电路 三相半波…

Java数据存储结构——二叉查找树

文章目录 22.1.2二叉查找树22.1.2.1 概述22.1.2.1二叉查找树添加节点22.1.2.2二叉查找树查找节点22.1.2.3 二叉树遍历22.1.2.4 二叉查找树的弊端 22.1.2二叉查找树 22.1.2.1 概述 二叉查找树,又称二叉排序树或者二叉搜索树 二叉查找树的特点&#xff1a; 每一个节点上最多有…