当前位置: 首页 > article >正文

c++的特性——多态

目录

概念

多态实现条件

虚函数 

 虚函数的重写/覆盖

练习题

 析构函数的重写

override和final关键字

重载/隐藏/重载的区别

纯虚函数和抽象类

多态

虚函数表指针

 多态的原理 

动态绑定与静态绑定

虚函数表总结


前面学习了C++的三个特性中的两个特性,今天我们将学习它的最后一个特性——多态。

概念

多态,实际上就是多种形态,多态分为编译时多态(静态多态)和运行时多态(动态多态),

静态多态就是函数重载和函数模板。因为它们实参传给形参的参数匹配是在编译时完成的·。

动态多态就是传不同对象给函数就会完成不同的行为。

比如买票的行为,学生买的是学生票,普通人买的是全价票,军人优先买票。

#include<iostream>
using namespace std;
class Person
{
public:virtual void func(){cout << "全价购票" << endl;}
};
class Student:public Person
{
public:virtual void func(){cout << "学生优惠票" << endl;}
};
class Soldier:public Person
{
public:virtual void func(){cout << "优先购票" << endl;}
};
void BuyTicket(Person& person)
{person.func();
}
int main()
{Student John;BuyTicket(John);Soldier Amy;BuyTicket(Amy);return 0;
}

在上面我们发现Student类和Solider类都继承了Person类,但是它们中都有一个一模一样的成员函数,而且父类的函数并没有被隐藏,这是为什么呢?下面我们就来学习多态实现的条件。

多态实现条件

有两个必需条件:

1.基类的指针或者引用必须调用虚函数

2.被调用的函数必须是虚函数,并且完成了虚函数的重写/覆盖。

虚函数 

类中成员函数前面用virtual修饰的为虚函数。注意:非成员函数不能用virtual修饰。

 虚函数的重写/覆盖

虚函数的重写/覆盖:派⽣类中有⼀个跟基类完全相同的虚函数(即派⽣类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称派⽣类的虚函数重写了基类的虚函数。
注意:在重写基类虚函数时,派⽣类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派⽣类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使⽤,不过在考试选择题中,经常会故意买这个坑,让你判断是否构成多态。

练习题

以下程序输出结果是什么()
A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确

class A
{
public:
virtual void func(int val = 1){ std::cout<<"A->"<< val <<std::endl;}
virtual void test(){ func();}
};
class B : public A
{
public:
void func(int val = 0){ std::cout<<"B->"<< val <<std::endl; }
};
int main(int argc ,char* argv[])
{
B*p = new B;
p->test();
return 0;
}

答案:B 解析:在test函数中调用了基态指针(*this)调用了虚函数(func),虽然派生类B类中func函数,没有virtual,但是还是和基类的func还是构成重写,所以存在多态。在test函数中A类指针指向的func函数,但是是B对象指向test函数,所以会调用B中的实现方式。所以val=1。

 析构函数的重写

像以下这段代码

class A
{
public:~A(){cout << "~A()" << endl;}
};
class B : public A
{
public:~B(){cout << "~B()" << endl;}
};
int main(int argc, char* argv[])
{A* a1 = new A;A* a2 = new B;delete a1;delete a2;return 0;
}

运行结果:

派生类 B 可能会分配额外的资源,如动态内存、文件句柄、网络连接等。如果只调用基类 A 的析构函数,而没有调用派生类 B 的析构函数,这些资源将无法被正确释放,从而导致内存泄漏或其他资源管理问题。我们发现在delete p2时,只调用了A的析构函数,没有调用B的析构函数。 但如果这两个类中存在多态关系就可以完全释放,只有将基类的析构函数声明为虚函数,才能保证在通过基类指针删除派生类对象时,能够正确地调用派生类的析构函数,进而先完成派生类的析构操作,再完成基类的析构操作,确保对象能够被完全销毁。那如何构成多态关系呢?

首先需要虚函数重写:

基类的析构函数为虚函数,此时派生类只要定义了,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同,但实际上编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理为destructor,所以基类的析构函数加了virtual修饰,派生类的析构函数就构成了重写。

override和final关键字

C++对虚函数重写的要求比较严格,但有些情况下由于疏忽,比如函数名写错参数等导致无法构成重写,而这种错误在编译期间是不会报出,只有在程序运行时没有得到预期结果才debug,会得不偿失,因此C++11提供override来帮助用户检测是否冲重写。如果不想让派生类重写这个虚函数,那么可以用final去修饰。

override关键字演示:override加在派生类虚函数后面。 

class Car {
public:virtual void Dirve(){}
};
class Benz :public Car
{
public:virtual void Drive()override//与基类的虚函数名称不同,不构成重写
//报错:错误	C3668	“Benz::Drive”: 包含重写说明符“override”的方法没有重写任何基类方法{cout << "Benz" << endl;}
};
int main()
{return 0;
}

final关键字演示:加在基类成员函数名后面 

class Car {
public://错误	C3248	“Car::Drive” : 声明为“final”的函数无法被“Benz::Drive”重写	virtual void Drive()final{}
};
class Benz :public Car
{
public:virtual void Drive(){cout << "Benz" << endl;}
};
int main()
{return 0;
}

重载/隐藏/重载的区别

重载:

1.两个函数作用在同一作用域。

2.函数名相同,参数不同,参数的类型或者个数不同,返回值可同可不同

重写/覆盖:

1.两个函数分别在继承体系中的父类和子类,在不同作用域

2.函数名、参数,返回值都必须相同,协变例外(基类虚函数返回基类对象的指针或者引
⽤,派⽣类虚函数返回派⽣类对象的指针或者引⽤时,称为协变。)

3.两个函数必须是虚函数

隐藏:

1.两个函数分别在继承体系的父类和子类中,在不同作用域

2.函数名相同/父类和子类的成员变量相同

3.两个函数只要不构成重写,就是隐藏

纯虚函数和抽象类

纯虚函数:

1.在虚函数的后面加上“=0”。

2.不需要定义实现,只要声明即可。

3.可以强制派生类重写虚函数,因为不重写就实例化不出对象。

抽象类:包含纯虚函数的类。抽象类不能实例化出对象,如果派生类继承后不重写纯虚函数,那么派生类也是抽象类。

class Car {
public:virtual void Drive()=0{}
};
class Benz :public Car
{
public:virtual void Drive(){cout << "Benz" << endl;}
};
class BMW :public Car
{
public:virtual void Drive(){cout << "BMW" << endl;}
};
int main()
{//Car car;//抽象类不能实例化出对象//但是如果派生类重写纯虚函数,派生类可以实例化出对象Benz benz;BMW bmw;Car* pBenz = new Benz;pBenz->Drive();Car* pBMW = new BMW;pBMW->Drive();return 0;
}

派生类中不重写虚函数,派生类也是抽象类。

class Car {
public:virtual void Drive()=0{}
};
class Benz :public Car
{
public:};
class BMW :public Car
{
public:virtual void Drive(){cout << "BMW" << endl;}
};
int main()
{Benz benz;
//报错:Benz类中没有重写抽象类中的纯虚函数,Benz也是抽象类
//故Benz不能实例化出对象BMW bmw;return 0;
}

多态

虚函数表指针

下⾯编译为32位程序的运⾏结果是什么()
A. 编译报错 B. 运⾏报错 C. 8 D. 12

class Base
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
protected:
int _b = 1;
char _ch = 'x';
};
int main()
{
Base b;
cout << sizeof(b) << endl;
return 0;
}

答案是D,如果我们按正常计算都应该是8byte,但12byte是怎么来的呢?

当我们调试,在监视窗口可以看到b对象多了一个成员变量(_vfptr)。

对象中的这个指针就是虚函数表指针。一格含有虚函数的类中都至少有一个虚函数表指针,因为一个类所有虚函数的地址要放进这个类对象的虚函数表中。

 多态的原理 

class Person {
public:
virtual void BuyTicket() { cout << "买票-全价" << endl; }
private:
string _name;
};
class Student : public Person {
public:
virtual void BuyTicket() { cout << "买票-打折" << endl; }
private:
string _id;
};
class Soldier: public Person {
public:
virtual void BuyTicket() { cout << "买票-优先" << endl; }
private:
string _codename;
};
void Func(Person* ptr)
{
// 这⾥可以看到虽然都是Person指针Ptr在调⽤BuyTicket
// 但是跟ptr没关系,⽽是由ptr指向的对象决定的。
ptr->BuyTicket();
}
int main()
{
// 其次多态不仅仅发⽣在派⽣类对象之间,多个派⽣类继承基类,重写虚函数后
// 多态也会发⽣在多个派⽣类之间。
Person ps;
Student st;
Soldier sr;
Func(&ps);
Func(&st);
Func(&sr);
return 0;
}

从底层结构的角度,Func函数中ptr->BuyTicket(),是如何让ptr指向Student对象调用Student::BuyTicket()的呢?我们知道如果想要找到函数,那么就要有函数的地址,但是这次我们不再是编译时通过调用对象确定函数的地址了,在上面我们谈到类中有虚函数的类的成员变量就会多一个虚函数表指针,所以我们在运行时通过虚函数表指针就可以确定虚函数的地址,这样就实现了指针或引用指向基类就调用基类的函数,指向派生类就调用派生类对应的虚函数。

在图中我们发现每个对象中的虚函数指针都是不同的,那么就可以区分该调用哪个对象的虚函数。 

动态绑定与静态绑定

静态绑定:对于非虚函数,在编译时确定调用函数地址。

动态绑定:对于虚函数,在运行时到指定对象的虚函数表中找到调用函数的地址。

//BuyTicket()是虚函数,编译在运行时到ptr的虚函数表中确定调用函数地址
ptr->BuyTicket();
00DA4931  mov         eax,dword ptr [ptr]  
00DA4934  mov         edx,dword ptr [eax]  
00DA4936  mov         esi,esp  
00DA4938  mov         ecx,dword ptr [ptr]  
00DA493B  mov         eax,dword ptr [edx]  
00DA493D  call        eax  
00DA493F  cmp         esi,esp  
00DA4941  call        __RTC_CheckEsp (0DA1302h) 
//BuyTicket()不是虚函数,编译器直接确定调用地址
ptr->BuyTicket();
003D3BB1  mov         ecx,dword ptr [ptr]  
003D3BB4  call        Person::BuyTicket (03D168Bh) 

虚函数表总结

1.基类对象的虚函数表中存放基类所有虚函数的地址。同类型的对象共用一张虚表,不同对象各自有独立的虚表。

2.派生类由两部分组成,继承下来的基类和自己的成员,一般情况下,继承下来的基类中有虚函数表指针,自己就不会在生成虚函数表指针。但是继承下来的基类部分虚函数表指针和基类对象的虚函数表指针不是同一个

3.派生类中重写的基类的虚函数,派生类的虚函数表中对应的虚函数就会被覆盖成派生类重写的虚函数地址。

4.派生类的虚函数表包含:(1)基类的虚函数表地址,(2)派生类重写的虚函数地址完成覆盖,(3)派生类自己的虚函数地址

5.虚函数表本质是一个存虚函数指针的指针数组。

6.虚函数和普通函数一样,编译后是一段指令,都是存放在代码段,只是虚函数表地址存到虚表中。

7.虚函数表存在哪里C++标准没有规定。但是可以通过代码对比验证。vs下存在代码段。

class Base {
public:virtual void func1() { cout << "Base func1" << endl; }virtual void func2() { cout << "Base func2" << endl; }
};class Derived : public Base {
public:void func1() override { cout << "Derived func1" << endl; }void func2() override { cout << "Derived func2" << endl; }
};
int main()
{Base b;Derived d;return 0;
}

相关文章:

c++的特性——多态

目录 概念 多态实现条件 虚函数 虚函数的重写/覆盖 练习题 析构函数的重写 override和final关键字 重载/隐藏/重载的区别 纯虚函数和抽象类 多态 虚函数表指针 多态的原理 动态绑定与静态绑定 虚函数表总结 前面学习了C的三个特性中的两个特性&#xff0c;今天我们…...

MySQL基础语法DDLDML

目录 #1.创建和删除数据库 ​#2.如果有lyt就删除,没有则创建一个新的lyt #3.切换到lyt数据库下 #4.创建数据表并设置列及其属性,name是关键词要用name包围 ​编辑 #5.删除数据表 #5.查看创建的student表 #6.向student表中添加数据,数据要与列名一一对应 #7.查询studen…...

性能测试理论基础-性能指标及jmeter中的指标

1、什么是性能测试 通过一定的手段,在多并发下情况下,获取被测系统的各项性能指标,验证被测系统在高并发下的处理能力、响应能力,稳定性等,能否满足预期。定位性能瓶颈,排查性能隐患,保障系统的质量,提升用户体验。 2、什么样的系统需要做性能测试 用户量大,页面访问…...

Postman CORS 测试完全指南:轻松模拟跨域请求,排查 CORS 相关问题

在使用 Postman 进行 API 测试时&#xff0c;通常不会遇到跨域问题&#xff0c;因为 Postman 是一个独立的客户端应用程序&#xff0c;不同于在浏览器中运行的 JavaScript 代码&#xff0c;它没有同源策略&#xff08;SOP&#xff09;的限制。跨域资源共享&#xff08;CORS&…...

iOS抓包-charles和Stream

简单介绍几种抓包工具 1、Charles Charles是一款流行的跨平台HTTP代理软件&#xff0c;常用于Web调试&#xff0c;它可以帮助你在开发过程中检查、修改或模拟HTTP/HTTPS请求和响应。以下是如何在iOS设备上使用Charles进行抓包的基本步骤&#xff1a; 第一步&#xff1a;安装…...

三个核心文件:src\App.vue文件,index.html文件,src\main.js文件 的关系与运行流程解析(通俗形象)

一、三个文件的角色定位 用生活比喻理解它们的关系&#xff1a; index.html → “空房子” 像一栋毛坯房&#xff0c;只有基本的墙面和预留的插座&#xff08;空白的HTML结构&#xff09;。它的作用是提供一个容器&#xff0c;告诉Vue&#xff1a;“请把装修好的房间&#xf…...

云原生系列-K8S实战

K8S实战 1. K8S 资源创建方式2. NameSpace 资源创建3. Pod4. Deployment5. Service6. Ingress7. 存储抽象1. 环境准备2. PV&PVC1&#xff09; 创建PV池2&#xff09; PVC创建与绑定 3. ConfigMap 抽取应用配置&#xff0c;并且可以自动更新1&#xff09; redis 示例2) 创建…...

从责任链模式聊到aware接口

从责任链模式聊到aware接口 责任链是什么&#xff1f; 责任链模式是一种行为型设计模式&#xff0c;将多个对象连接成一条链&#xff0c;并且沿着这条链传递请求&#xff0c;让多个对象都有机会处理这个请求&#xff0c;请求会顺着链传递&#xff0c;直到某个对象处理它为止。…...

ArcGIS地理信息系统空间分析实验教程学习

ArcGIS 作为地理信息系统领域的经典软件&#xff0c;以其强大的功能和广泛的应用场景&#xff0c;成为了众多学者、研究人员和专业人士的首选工具。它不仅可以高效地处理和可视化地理空间数据&#xff0c;还能通过复杂的空间分析模型&#xff0c;揭示地理现象背后的规律和趋势。…...

【3天!!!从0-1完成自动化集成平台开发--Cursor AI赋能0代码基础测试工程师开发平台-亲测有效-保姆级】

利用Cursor AI 赋能测试工程师从0-1开发自动化集成平台 ——含框架设计、实例代码与CI/CD集成 一、技术选型与框架设计1.1 核心框架选择1.2 整体架构图二、从0到1开发步骤2.1 初始化项目2.2 核心模块开发模块1:测试用例管理(Python)模块2:测试执行引擎(pytest)三、实战案…...

Compose 实践与探索十七 —— 多指手势与自定义触摸反馈

上一节我们讲了滑动的手势识别以及嵌套滑动&#xff0c;二者都属于触摸反馈这个大的范畴内的知识。本节我们将深入触摸反馈这个话题&#xff0c;讲一讲多指手势的识别与完全自定义的触摸反馈的实现。 1、多指手势 多指手势可以分为两类&#xff1a; 利用 API 处理预设好的手…...

Spring Boot 整合 ElasticJob 分布式任务调度教程

精心整理了最新的面试资料和简历模板&#xff0c;有需要的可以自行获取 点击前往百度网盘获取 点击前往夸克网盘获取 Spring Boot 整合 ElasticJob 分布式任务调度教程 一、ElasticJob 简介 ElasticJob 是当当网开源的分布式任务调度解决方案&#xff0c;支持&#xff1a; …...

Go 语言规范学习(6)

文章目录 StatementsTerminating statementsEmpty statementsLabeled statementsExpression statementsSend statementsIncDec statementsAssignment statementsIf statementsSwitch statementsExpression switchesType switches For statementsFor statements with single con…...

centos8上实现lvs集群负载均衡nat模式

1.背景&#xff1a; 个人&#xff08;菜鸟&#xff09;学习笔记&#xff0c;学点记下来&#xff0c;给未来的自己看。高手看了也请多指点。 按照课程讲&#xff0c;lvs是我国大神开发的负载均衡程序&#xff0c;被收录进内核&#xff0c;只要安装时内核里有它&#xff0c;它就…...

深度学习篇---模型参数调优

文章目录 前言一、Adam学习&#xff08;lr&#xff09;1. 默认学习率2. 较小的学习率模型复杂数据集规模小 3. 较大的学习率模型简单训练初期 4. 学习率衰减策略固定步长衰减指数衰减 二、训练轮数&#xff08;epoch&#xff09;1. 经验值设定小数据集与简单模型大数据集和复杂…...

影响HTTP网络请求的因素

影响 HTTP 网络请求的因素 1. 带宽 2. 延迟 浏览器阻塞&#xff1a;浏览器会因为一些原因阻塞请求&#xff0c;浏览器对于同一个域名&#xff0c;同时只能有4个连接&#xff08;这个根据浏览器内核不同可能会有所差异&#xff09;&#xff0c;超过浏览器最大连接数限制&…...

Openssl自签证书相关知识

1.前提 检查是否已安装 openssl $ which openssl /usr/bin/openssl 2.建立CA授权中心 2.1.生成ca私钥(ca-prikey.pem) 初始化 OpenSSL 证书颁发机构(CA)的序列号文件 在生成证书时,ca.srl 的初始序列号需正确初始化(如 01),否则可能导致证书冲突 这会将 01 显示在屏幕…...

浅析车规芯片软错误防护加固的重要性

随着汽车电子技术的飞速发展&#xff0c;汽车已经从传统的机械交通工具转变为高度依赖电子系统的智能移动终端。车规芯片作为汽车电子系统的核心部件&#xff0c;其可靠性和安全性直接关系到车辆的正常运行和驾乘人员的安全。然而&#xff0c;车规芯片在复杂的运行环境中面临着…...

(UI自动化测试web端)第二篇:元素定位的方法_css定位之css选择器

看代码里的【find_element_by_css_selector( )】( )里的表达式怎么写&#xff1f; 文章介绍了第三种写法css选择器&#xff0c;你要根据网页中的实际情况来判断自己到底要用哪一种方法来进行元素定位。每种方法都要多练习&#xff0c;全都熟了之后你在工作当中使用起来元素定位…...

QT自运行程序

终局 搞定了兄弟们,啥也别说了。 不要用xcb,用linuxfb。 用systemd服务。 海康威视的豆干型网络摄像头我这边尝试后,发现在multi-user.target运行级别下,摄像头登录成功了也采集不到画面。 具体愿意暂不清楚,所以如果是涉及摄像头的,建议…...

MPU6050模块详解:从原理到STM32驱动指南(上) | 零基础入门STM32第八十九步

主题内容教学目的/扩展视频加速度传感器电路连接。手册分析。驱动程序&#xff0c;读出数据。能读出3轴数据。 师从洋桃电子&#xff0c;杜洋老师 &#x1f4d1;文章目录 一、MPU6050模块介绍1.1 核心特性1.2 模块化优势 二、MPU6050模块连接方法2.1 硬件连接2.2 电源注意事项 …...

STM32 MODBUS-RTU主从站库移植

代码地址 STM32MODBUSRTU: stm32上的modbus工程 从站 FreeModbus是一个开源的Modbus通信协议栈实现。它允许开发者在各种平台上轻松地实现Modbus通信功能&#xff0c;包括串口和以太网。FreeMODBUS提供了用于从设备和主站通信的功能&#xff0c;支持Modbus RTU和Modbus TCP协…...

架构师面试(二十二):TCP 协议

问题 今天我们聊一个非常常见的面试题目&#xff0c;不管前端还是后端&#xff0c;也不管做的是上层业务还是底层框架&#xff0c;更不管技术方向是运维还是架构&#xff0c;都可以思考和参与一下哈&#xff01; TCP协议无处不在&#xff0c;我们知道 TCP 是基于连接的端到端…...

程序自动化填写网页表单数据

1 背景介绍 如何让程序自动化填写网页表单数据&#xff0c;特别是涉及到批量数据情况时&#xff0c;可以减少人力。下面是涉及到的一些场景&#xff0c;都可以通过相关自动化程序实现。 场景1 场景1&#xff0c;领导安排&#xff0c;通过相关省、市、县、乡镇数据&#xff0…...

Razer macOS v0.4.10快速安装

链接点这里下载最新的 .dmg 文件。将下载的 .dmg 映像文件拖入 应用程序 文件夹中。若首次打开时出现安全警告【什么扔到废纸篓】&#xff0c;这时候点击 Mac 的“系统偏好设置”-> “安全性与隐私”-> “通用”&#xff0c;然后点击底部的 “打开”。【或者仍然打开】 对…...

常用正则表达式-MAC 地址

MAC地址的定义 物理地址&#xff08;通常称为 MAC地址&#xff0c;Media Access Control Address&#xff09;是网络设备在数据链路层&#xff08;如以太网、Wi-Fi&#xff09;的唯一标识符。它由设备的网络接口卡&#xff08;NIC&#xff09;固化在硬件中&#xff0c;用于在局…...

如何自动化同义词并使用我们的 Synonyms API 进行上传

作者&#xff1a;来自 Elastic Andre Luiz 了解如何使用 LLM 来自动识别和生成同义词&#xff0c; 使术语可以通过程序方式加载到 Elasticsearch 同义词 API 中。 提高搜索结果的质量对于提供高效的用户体验至关重要。优化搜索的一种方法是通过同义词自动扩展查询词。这样可以更…...

一. 相机模组摆放原理

1. 背景&#xff1a; 相机开发时经常出现因模组摆放问题&#xff0c;导致相机成像方向异常。轻则修改软件、模组返工&#xff0c; 重则重新修改堆叠&#xff0c;影响相机调试进度。因此&#xff0c;设计一个模型实现模组摆放纠错很有必要。 2. 原理&#xff1a; 2.1 口诀&am…...

【C++游戏引擎开发】《线性代数》(1):环境配置与基础矩阵类设计

一、开发环境配置 1.1 启用C 20 在VS2022中新建项目后右键项目 1.2 启用增强指令集 1.3 安装Google Test vcpkg安装使用指南 vcpkg install gtest:x64-windows# 集成到系统目录&#xff0c;只需要执行一次&#xff0c;后续安装包之后不需要再次执行 vcpkg integrate inst…...

sqli-labs靶场 less 8

文章目录 sqli-labs靶场less 8 布尔盲注 sqli-labs靶场 每道题都从以下模板讲解&#xff0c;并且每个步骤都有图片&#xff0c;清晰明了&#xff0c;便于复盘。 sql注入的基本步骤 注入点注入类型 字符型&#xff1a;判断闭合方式 &#xff08;‘、"、’、“”&#xf…...