关于C++多态的复习总结

多态

简介: 面向对象的三大特性之一,多态顾名思义即具有多种形态,即去执行某个行为时,当不同的对象去执行时会产生不同的状态

构成多态的条件

条件一

必须通过基类(父类)的指针或者引用调用虚函数(函数被virtual所修饰)
tips:父类的指针或引用要指向或引用子类对象

virtual void test(){}

条件二

被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
tips:破坏任意一个条件都会导致无法构成多态

  • 虚函数的重写是接口继承(普通函数的重写是实现继承)
    ps:普通函数的重写是外壳不同,将函数体(实现)继承下来
  • 子类中的虚函数只是对父类的接口的一个声明(声明必须保持一致,故函数名,形参以及返回值都相同),重写只是将父类函数的“外壳”拿了下来,然后再在这个“外壳”内填充函数体(重写的是实现)

满足上述两个条件即构成多态:即通过基类(父类)的指针或者引用调用虚函数
子类没重写也会进行运行时决议(多态),但是由于没有进行覆盖,仍旧调用的是父类的虚函数

虚函数重写/覆盖的条件

虚函数+三同(函数名,形参以及返回值都相同),不符合重写的条件即构成隐藏

  • tips1:子类重写虚函数时,是否添加virtual修饰对多态不影响

  • tips2:重写的协变,协变即返回值可以不同,但要求父子函数的返回值必须分别是父子关系的指针或者引用
    如下述两种方式都是可以构成多态(此种用途并不多,了解即可)

    class Person
    {
    public:
     //virtual void BuyTicket(char) { cout << "买票-全价" << endl; }
     /*virtual Person* BuyTicket(int) 
     { 
     	cout << "买票-全价" << endl;
     	return this;
     }*/
     //假设A是B的父类,即下述也构成多态
     virtual A* BuyTicket(int)
     {
     	cout << "买票-全价" << endl;
     	return nullptr;
     }
    };
    
    class Student : public Person {
    public:
     // 虚函数重写/覆盖条件 : 虚函数 + 三同(函数名、参数、返回值)
     // 不符合重写,就是隐藏关系
     // 特例1:子类虚函数不加virtual,依旧构成重写 (实际最好加上)
     // 特例2:重写的协变。返回值可以不同,要求必须时父子关系的的指针或者引用
     /*virtual Student* BuyTicket(int)
     {
     	cout << "买票-半价" << endl;
     	return this;
     }*/
    
     virtual B* BuyTicket(int)
     { 
     	cout << "买票-半价" << endl;
     	return nullptr;
     }
    };
    

构成多态的原理

虚表(虚函数表)

  • 虚表内存储着所有的虚函数(函数地址),虚表本质是一个数组,数组内存着的都为函数指针

    • 父类对象和子类对象里各自都有各自的虚表
      子类对象的虚表是拷贝父类对象得来
    • 当子类重写虚函数时,则修改了子类自己的虚表对应的函数(覆盖成子类重写后的函数)

虚表指针

当类内存在了virtual修饰的虚函数,则该类内会默认生成一个虚表指针(__vfptr)指向一张虚表
虚表指针在vs环境下,默认是在对象的头4个字节或者头8个字节(可以通过取出该字节的内容所指向的地址来打印虚表)
ps: 虚函数表是编译时即生成的,在构造函数中进行初始化虚表指针,对象中存储的为虚表指针,虚表存储位置大致在常量区(编译阶段即生成好了)

  • tips:可以按下述方式尝试打印虚表内的内容

    class Person 
    {
    public:
    	virtual void BuyTicket() 
    	{ 
    		cout << "Person::买票-全价" << endl;
    	}
    
    	virtual void Func1()
    	{
    		cout << "Person::Func1()" << endl;
    	}
    };
    
    class Student : public Person {
    public:
    	virtual void BuyTicket() 
    	{ 
    		cout << "Student::买票-半价" << endl;
    	}
    
    	virtual void Func2()
    	{
    		cout << "Student::Func2()" << endl;
    	}
    };
    
    typedef void(*VFPTR)();
    
    //void PrintVFTable(VFPTR table[])
    //void PrintVFTable(VFPTR* table, size_t n)
    void PrintVFTable(VFPTR* table)
    {
    	//vs下,虚表末尾会加上空指针作为标识
    	for (size_t i = 0; table[i] != nullptr; ++i)
    	//for (size_t i = 0; i < n; ++i)
    	{
    		printf("vft[%d]:%p->", i, table[i]);
    		//table[i]();
    		VFPTR pf = table[i];
    		pf();
    	}
    	cout << endl;
    }
    int main()
    {
    	// 同一个类型的对象共用一个虚表
    	Person p1;
    	Person p2;
    
    	// vs下 不管是否完成重写,子类虚表跟父类虚表都不是同一个
    	Student s1;
    	Student s2;
    	//取到对象头四个字节的虚表指针中的函数地址,再强转成函数指针(因为本身就是函数地址)
    	PrintVFTable((VFPTR*)*(int*)&s1);
    	PrintVFTable((VFPTR*)*(int*)&p1);
    }
    

普通多继承下的情况

  • 多继承中,子类新增的虚函数会被放到多继承下来的第一个对象的虚表里
    多继承的对象有几个,则子类中有多少个虚表(从父类继承得来)
    • 多继承下,子类自身的虚函数会被放到多继承第一个对象的虚表中
    • 多继承下,不同的父类指针指向子类对象并调用虚函数时,底层实现略有不同,不过到底也是相同的
      因为调用函数时,本质也要传入指向对象的地址,继承的两个基类所在的地址不同,故此底层跳转步骤不尽相同
      • 如果是第一个继承的对象,则是直接进行call函数地址然后jump到函数实现
      • 如果是第二个继承的对象,则会先call指令,然后会先偏移到子类对象的首地址处,再进行jump

菱形继承下的情况

  • 最开始菱形继承中,如果菱形继承的两个父类没有额外的虚函数,则是共用基类的虚表(通过虚基表指向)

  • 如果菱形继承下,两个父类还有额外的虚函数,则父类其还会拥有自己的虚表指针(指向虚表)

  • 虚基表:虚继承中产生的虚基类表(解决数据冗余和二义性)

    • 虚基表的记录的内容其一是当前派生类的虚基表与其虚表的偏移量(如果不存在额外的虚表则偏移量为0)
      为了让派生类能够找到其虚表的位置
    • 其二是记录虚基类与其派生类在当前对象模型中的偏移地址
  • 只要是虚函数,函数地址都会放入虚表中,无论是否被重写,子类的虚表中既有父类的虚函数,也有子类的虚函数
    tips:同一个类型的对象共用一个虚表,(vs环境)子类和父类的虚表不管是否完成重写,二者虚表都不是同一个

总结

多态的本质即当符合多态的两个条件,调用时则会到指向对象的虚表中找到对应的函数地址进行调用
故此多态的调用时运行时才通过虚表确定了函数的地址,编译时并不知道会自身会指向父类还是子类的对象,运行到了才会到实际指向对应对象的虚表内找到函数地址再进行调用
(普通函数的调用是调用call指令,在编译链接时确定了函数的地址(声明+定义),运行时直接调用)


析构函数的重写

父类的析构函数在继承中建议添加virtual修饰,完成虚函数的重写

class Person{
	public:
		virtual ~Person(){cout << "~Person()" << endl;}
}
class Student{
	public:
		virtual ~Student(){cout << "~Student()" << endl;}
}
int main(){
	Person* ptr1 = new Person();
	delete ptr1;
	//如果不构成多态,则是什么类型即调用什么类型的析构函数,不符合预期
	Person* ptr2 = new Student();
	delete ptr2;
	//构成多态则指向父类调用父类析构,指向子类调用子类析构,子类析构后再自动调用父类的析构函数
}
  • 编译器生成的析构函数的作用(同上)
    自己调用自己的析构,父类对象去调用父类的析构
    • 由于多态的需要,析构函数的名字会被统一处理成destructor()
      所以析构函数也会与父类构成隐藏
    • 由于语法与编译器要求,构造时需要先构造父类,再构成子类,析构则需要保证先析构子类,再析构父类,所以自定义析构函数时不需要显式调用父类的析构,编译器会在析构完子类后自动调用父类的析构

override和final关键字

  • 当一个虚函数不想被重写,则使用final进行修饰(使用场景极少)
  • override用于修饰子类的虚函数,其对子类的虚函数是否重写进行了强制性语法检查

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

  • 重载
    • 两个函数在同一作用域
    • 函数名相同,参数不同(个数,类型,顺序)
  • 重写(覆盖)
    • 两个函数分别在基类和派生类的作用域
    • 函数名,参数,返回值都必须相同(协变属于例外)
    • 两个函数必须都是虚函数(用virtual修饰)
  • 重定义(隐藏)
    • 两个函数分别在基类和派生类的作用域
    • 函数名相同
    • 基类与派生类的同名函数不构成重写即为重定义(隐藏)

抽象类

在虚函数后面写上=0,则这个函数为纯虚函数,包含这个纯虚函数的类即为抽象类(也被成为接口类)

  • 抽象类基类是无法实例化出对象的
  • 子类继承了纯虚类后,子类必须得进行虚函数的重写,否则也无法实例化对象

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

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

相关文章

家用洗地机哪款最好用?附热门洗地机品牌推荐,看完这篇不踩坑

随着技术的不断发展&#xff0c;现在的洗地机功能已经越来越强大了&#xff0c;它可以高效的扫地、拖地、不用手动清洗滚刷&#xff0c;甚至有些中高端型号还能边洗地边除菌&#xff0c;远程操控自清洁&#xff0c;简直就是家居清洁的小能手&#xff01;那么&#xff0c;家用洗…

‘vue-cli-service‘ is not recognized as an internal or external command解决方案

vue-cli-service is not recognized as an internal or external command, operable program or batch file.解决方案 先进行 &#xff1a; npm install -g vue/cli 命令安装vue cli 是必须的。 如果 npm run build 还是报错 遇到同样的提示&#xff1a; 这时候先安装依赖 np…

深入理解与应用C++ Vector

1. C Vector 简介与基本使用 C 的 vector 是一个序列容器&#xff0c;用于表示可变大小的数组。它结合了数组的高效元素访问和动态大小调整的灵活性。与静态数组相比&#xff0c;vector 的大小可以根据需要自动调整&#xff0c;这是通过在底层使用动态数组来实现的。当新元素被…

返回倒数第K个节点(C语言)———链表经典算法题

题目描述​​​​​​&#xff1a;面试题 02.02. 返回倒数第 k 个节点 - 力扣&#xff08;LeetCode&#xff09;&#xff1a; 答案展示: /*** Definition for singly-linked list.* struct ListNode {* int val;* struct ListNode *next;* };*/int kthToLast(struct Li…

娱乐营销的新玩法:Kompas.ai如何让内容更加趣味化

在数字化时代&#xff0c;内容营销已成为品牌与消费者沟通的重要桥梁。然而&#xff0c;随着信息的爆炸式增长&#xff0c;用户的注意力越来越分散&#xff0c;传统的营销方式已经难以吸引用户的兴趣。在这种背景下&#xff0c;娱乐营销应运而生&#xff0c;它通过将娱乐元素融…

2023年30米分辨率土地利用遥感监测数据

改革开放以来&#xff0c;中国经济的快速发展对土地利用模式产生了深刻的影响。同时&#xff0c;中国又具有复杂的自然环境背景和广阔的陆地面积&#xff0c;其土地利用变化不仅对国家发展&#xff0c;也对全球环境变化产生了深刻的影响。为了恢复和重建我国土地利用变化的现代…

R语言:GSEA分析

#安装软件包 > if (!requireNamespace("BiocManager", quietly TRUE)) install.packages("BiocManager") > BiocManager::install("limma") > BiocManager::install("org.Hs.eg.db") > BiocManager::install("…

【回溯 栈 代数系统 动态规划】282. 给表达式添加运算符

本文涉及知识点 回溯 栈 代数系统 动态规划 LeetCode 282. 给表达式添加运算符 给定一个仅包含数字 0-9 的字符串 num 和一个目标值整数 target &#xff0c;在 num 的数字之间添加 二元 运算符&#xff08;不是一元&#xff09;、- 或 * &#xff0c;返回 所有 能够得到 ta…

C++深度解析教程笔记8

C深度解析教程笔记8 第17课 - 对象的构造&#xff08;上&#xff09;类定义中成员变量i和j的初始值&#xff1f;实验-成员变量的初始值对象初始化解决方案1实验-手动调用函数初始化对象对象初始化解决方案2&#xff1a;构造函数实验-构造函数小结 第18课 - 对象的构造&#xff…

File类~路径、创建文件对象

路径分为相对路径&#xff08;不带盘符&#xff09;&#xff0c;绝对路径&#xff08;带盘符&#xff09; 路径是可以存在的&#xff0c;也可以是不存在的 创建文件对象的三个方法&#xff1a;

QT设计模式:策略模式

基本概念 策略模式&#xff08;Strategy Pattern&#xff09;是一种行为型设计模式&#xff0c;它定义了一系列方法&#xff0c;并使它们可以相互替换。策略模式使得算法可以独立于客户端而变化&#xff0c;使得客户端可以根据需要选择相应的算法。 策略模式通常由以下角色组…

使用非官网购买Chatgpt的api调用

测试代码 from openai import OpenAI client OpenAI(api_key用户密钥) import json import os import timeclass ChatGPT:def __init__(self, user):self.user userself.messages [{"role": "system", "content": "Agent"}]def as…

每周一算法:传递闭包

题目描述 不等式排序 给定 n n n个变量和 m m m个不等式。其中 n n n小于等于 26 26 26&#xff0c;变量分别用前 n n n 的大写英文字母表示。 不等式之间具有传递性&#xff0c;即若 A > B A>B A>B 且 B > C B>C B>C&#xff0c;则 A > C A>C …

linux下的进程通信

进程通信 进程为什么需要通信呢&#xff1f;进程通信的技术背景进程通信本质 进程通信分类管道匿名管道pipe匿名管道原理管道特点 命名管道创建命名管道命名管道原理 System V IPC管道与 System V的区别共享内存函数ftok()shmget() shmat()shmdt()shmctl()删除共享内存System V…

【笔记】EF_PNN获取及运营商名称显示(待完善)

问题背景 当设备无法成功解析EONS(PNN)的值(即SIM卡EF文件内容),则会用次优先级的NITZ去refresh了SPN。(问题代码如下,是通过Phone对象拿到plmn为空) 运营商名称一般显示优先级:Eons > NITZ > XML OPL id 0 对应的是PNN第一条 功能逻辑 (定制)当卡中的spn为空…

生产制造行业推拉式生产的复合应用

一、案例分析&#xff08;汽配行业&#xff09; 重点&#xff1a; 1. MTO/MTS 与 PUSH/PULL 有关系但是不是充分关系 2. MTO/MTS 是公司经营策略&#xff0c;更多是对市场需求的经营策略&#xff0c;体现在生产时机上的不同&#xff0c;一个是等客户需求&#xff0c;一个是填…

做国外问卷调查,一天能挣多少钱?

大家好​&#xff0c;我是汇舟问卷&#xff0c;专注于国外问卷调查项目已经五年的时间了&#xff0c;目前做的一直比较稳定。 这个项目说白了就是通过搭建国外的环境&#xff0c;登录问卷平台&#xff0c;通过参与国外企业发布的问卷调查来获取​美金奖励。 那么参与的问卷的…

AI算法-高数5.2-线性代数-向量间的线性相关、无关定义和结论

宋浩老师课程&#xff1a;3.2 向量间的线性关系&#xff08;二&#xff09;_哔哩哔哩_bilibili 线性相关、不相关结论&#xff1a; 判断线性有关\无关&#xff0c;转化成方程组&#xff1a; 判断条件> 向量线性相关、无关的本质是&#xff1a;除0外能不能找到非0的数据。

交流负载箱:电力系统的智能升级

随着科技的不断发展&#xff0c;电力系统也在不断地进行升级和改进。在这个过程中&#xff0c;交流负载箱作为一种新型的电力设备&#xff0c;为电力系统的智能升级提供了有力的支持。本文将对交流负载箱在电力系统中的应用及其优势进行简要分析。 首先&#xff0c;交流负载箱…

【Qt】常用控件(一)

文章目录 一、核心属性1、enabled代码示例: 通过按钮2 切换按钮1 的禁用状态 2、geometry代码示例: 控制按钮的位置代码示例&#xff1a;window frame 的影响代码示例: 感受 geometry 和 frameGeometry 的区别 3、windowTitle4、windowIcon代码示例: 通过 qrc 管理图片作为图标…
最新文章