C++进阶——多态

目录

一、多态的概念

二、多态的实现

1.逻辑条件

2.代码层面

3.一个经典题目

4.虚函数重写的其它问题

4.1协变(了解)

4.2析构函数重写

4.3 override和final

4.4重载、重写(覆盖)和隐藏的对比

5.纯虚函数和抽象类

三、多态的原理

1.虚函数表指针

2.多态的原理

 3.动态绑定和静态绑定

4.虚函数表(细节)


一、多态的概念

多态,如它本身所表达的意思,就是一种事物多种状态、形态。

多态分为编译时多态和运行时多态。

编译时多态(也称静态多态)其实我们之前已经了解很多了,函数模板和函数重载就是,通过函数模板和函数重载,我们可以达到一种传不同的参数使“同一个函数”进行对应的不同的操作的状态,由于这种多态在编译时函数所需要传递的参数就已经确定了,所以叫做编译时多态,也称作静态多态。

运行时多态(也称动态多态)具体点其实就是我们的程序需要去完成某个行为,比如购票,如果传学生对象,那么就打折,如果传军人对象,那么就优先购票,如果传普通人,那么就是没有任何优惠。程序会根据我们传递的对象而在运行时展现出不同的行为,所以叫做运行时多态,也叫动态多态。

二、多态的实现

1.逻辑条件

多态其实就是同一基类的派生类对象,调用同一函数,而产生了不同的结果。

这就是多态实现的逻辑条件,比如军人和学生继承自Person,都调用购票函数,但是产生的结果不同。

2.代码层面

根据C++的设计,代码层面要实现多态需要做到两点:

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

2.派生类对基类的虚函数进行重写(覆盖)

#include <iostream>
using namespace std;class Person
{
public:// 实现多态基类必须有虚函数virtual void buy_ticket(){cout << "购票-正常" << endl;}
};class Student : public Person
{
public:// 实现多态必须让派生类重写基类虚函数// 重写:“三同”函数名、参数列表、返回类型virtual void buy_ticket(){cout << "购票-打折" << endl;}
};// 为什么必须传基类的指针或引用?
// 因为这样不管是传派生类对象还是基类对象,都会统一切片成基类对象
// 也就是说这样的方式可以保证基类对象和派生类对象都可以调用此函数
// 而如果形参是派生类的指针或引用,传基类的对象就不得行
void func(Person *p)
{// 这里Student不是被切片成基类对象了吗,怎么调用出来会显示打折?// 后面的虚函数表会解释,大家先别急,现在只需要知道这么可以实现多态即可p->buy_ticket();
}void func(Person& p)
{p.buy_ticket();
}int main()
{Student s;Person p;func(&s);  //打折func(s);	    //打折func(p);	//正常return 0;
}

3.一个经典题目

猜编译结果:

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()
{B* p = new B;p->test();return 0;
}

输出:B->1

没错!!!!本博主没敲错!!!!!!

看我的注释的解释:

class A
{
public:virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }virtual void test() { func(); }
};
class B : public A
{
public:// 在重写基类虚函数时,派⽣类的虚函数在不加virtual关键字时,虽然也可以构成重写// 因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性// 但是该种写法不是很规范,不建议这样使用,不过在考试选择题中,经常会故意埋这个坑,让你判断是否构成多态。// 所以这里其实是构成多态的void func(int val = 0) { std::cout << "B->" << val << std::endl; }
};
int main()
{B* p = new B;p->test();// test没有重写,test不构成多态,test的本质是virtual void test(A* this){}// 参数列表中A是基类,A*是基类对象的指针,所以构成了多态// 所以调用test()时,会走到B重写的func中,所以是B->?// 其实很多人都误以为是B->0,但其实是B->1// 虚函数的重写,缺省值确实不用一样,但是其实本质上是对函数体的重写,而没有重写参数列表,更没有重写缺省值// 所以这里多态的缺省值应该是1p->func(); // 这个的输出结果是B->0// 为什么不是B->1呢?// 因为这里不是多态的用法,func的参数缺省值就是0,只有当多态时才和基类一样// 所以说,这里的细节太多,容易失误,在具体写代码的时候要避免在派生类重写的虚函数中定义或修改缺省值return 0;
}

4.虚函数重写的其它问题

4.1协变(了解)

简单说,因为协变没什么实际意义,可能是祖师爷多喝了几杯......

派生类重写的虚函数,与基类的虚函数返回值可以不同,但是必须返回该类的指针或引用。

派生类返回派生类的指针或引用,基类返回基类的指针或引用。

4.2析构函数重写

基类的析构函数默认就是虚函数,而派生类的析构函数一旦定义,无论是否显示写virtual关键字,该析构函数也默认是虚函数,而且与基类的析构函数构成重写。

嗯?不是函数名不一样吗?

这是因为编译器对其进行了特殊处理,编译处理后所有的析构函数都会被处理成函数名为destructor,所以便构成了重写。

为什么这么玩?看代码与我写的注释:

class A
{
public:virtual~A(){cout << "~A()" << endl;}
};
class B : public A {
public:~B(){cout << "~B()->delete:" << _p << endl;delete _p;}
protected:int* _p = new int[10];
};int main()
{A* p1 = new A;A* p2 = new B;// 回忆一下delete:先调用对象的析构函数,再释放申请的空间delete p1;delete p2;// 如果A的析构函数不是虚函数// p1和p2都是A*类型,调用析构函数都是调用的~A()// 但是delete p2是想释放一个B对象的空间,B对象的空间里有需要B的析构函数释放的资源// 所以就会造成内存泄漏// 如果A的析构函数是虚函数// 那么p2调用析构函数,A和B的析构函数构成了多态,所以会调用B的析构函数,释放B对象申请的资源// 而且B的析构函数调用完成之后也会自动调用A的析构函数,不会造成内存泄漏// 所以综上所述,我们在写基类的构造函数时最好还是将析构函数定义成虚函数return 0;
}

4.3 override和final

override关键字:有些时候,我们由于自身的疏忽,比如写错函数名等,以至于虚函数并没有重载,但是这个错误如果一开始没发现,我们在后面的运行阶段才能发现错误,再去debug,就会很浪费时间,所以C++11新增了override关键字,在函数的参数列表后面加上override关键字,编译器就会帮我们检查该函数是否重写了其它的虚函数,如果没有,那么就会直接报错。

// error C3668: “Benz::Drive”: 包含重写说明符“override”的⽅法没有重写任何基类⽅法
class Car {
public:virtual void Dirve(){}
};
class Benz :public Car {
public:// 如果函数名写错了,预编译阶段就会直接报错virtual void Dirve() override { cout << "Benz-舒适" << endl; }
};
int main()
{Benz b;Car* p = &b;p->Dirve();return 0;
}

final关键字:之前在学继承的时候,如果这个类不想被继承,就可以在类名后面加个final修饰,这样如果有继承了该类的类,在编译阶段就会报错。这里也是一样,在虚函数的参数列表后面加上final这个关键字修饰,该虚函数就不能被重写,如果重写了,就会直接报错。

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

4.4重载、重写(覆盖)和隐藏的对比

1.重载是同一作用域中,同名函数的多中实现方式,函数名相同,但是参数的传递不同,传不同的参数会调用不同的函数(参数类型、数量可能不同),返回值无吊所谓。

2.重写(覆盖)是在不同作用域下,派生类对基类中虚函数的一种类似于重新定义。两个函数都是虚函数,函数名,参数列表和返回值都相同(除了协变和析构函数这种特殊情况),实现不同。

3.隐藏是在不同作用域下,函数名或者变量名相同即可(当然前提也得是继承)。

5.纯虚函数和抽象类

纯虚函数:在虚函数的参数列表后面加上“ = 0”,这个虚函数就变成了纯虚函数,纯虚函数不需要被实现,(实现了也没啥意义),主要是为了让派生类重写。

抽象类:具有纯虚函数的类,就是抽象类,抽象类不能实例化出对象,作用是供别的类继承。

注意:如果派生类继承了抽象类而没有重写抽象类中的纯虚函数,那么这个派生类也是抽象类,因为基类中的纯虚函数被继承了下来。

class Person
{
public:// 纯虚函数,不需要实现virtual void buy_ticket() = 0;// 拥有了纯虚函数,Person就成了抽象类
};class Student : public Person
{
public:// 如果不重写Person的纯虚函数,那么Student就会从Person继承纯虚函数,也会变成抽象类virtual void buy_ticket(){cout << "购票-打折" << endl;}
};class Soldier : public Person
{
public:virtual void buy_ticket(){cout << "购票-优先" << endl;}
};void func(Person *p)
{p->buy_ticket();
}void func(Person& p)
{p.buy_ticket();
}int main()
{//Person p;// "Person" : 无法实例化出抽象类//Student s;//Soldier soldier;//Person* p1 = &s;//Person* p2 = &soldier;// 上面的可以一次性写成下面这种形式Person* p1 = new Student;Person* p2 = new Soldier;func(p1);func(p2);return 0;
}

三、多态的原理

1.虚函数表指针

对一个具有虚函数的类,调试时进入监视窗口就会发现,有个我们不认识的东西:

虚函数表指针,指向的是一个函数指针数组,这个数组中存了该类中所有虚函数的指针,这个数组叫虚表(虚函数表)。

2.多态的原理

看图说话:

 3.动态绑定和静态绑定

静态绑定:不满足多态条件的虚函数调用(使用指针或引用),在编译阶段就会确定虚函数的地址,所以是静态绑定。
动态绑定:满足多态条件的虚函数调用(使用指针或引用),在运行时在指向对象的虚表中寻找虚函数的地址,所以时动态绑定。

4.虚函数表(细节)

派生类在刚开始会和基类有一个相同的虚函数表。

1.当派生类对基类中的虚函数进行重写时,派生类的虚函数表会发生改变(重写的虚函数覆盖掉原来的虚函数地址)

2.当派生类增添新的虚函数时,派生类的虚函数表会增添一个虚函数地址

综合1、2,可以知道,派生类的虚函数表包括三部分的地址:基类的虚函数(除重写)、重写的基类的虚函数、派生类新添加的虚函数。

3.虚函数表一般情况下会放一个0x00000000的标志表明虚函数表的末尾,但是C++标准并没有规定。(vs是这样的,gcc不是)。

 

4.虚函数跟普通函数一样,编译后是一段指令,都存在代码段,只不过是地址存到了虚表。

5.虚函数表的存储位置,C++没有明确规定,但是可以通过这段代码,来通过比较看出来(相同区域地址类似,况且这只是个小程序而已,挨得都比较近)

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.rhkb.cn/news/447665.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

k8s的部署

一、K8S简介 Kubernetes中文官网&#xff1a;Kubernetes GitHub&#xff1a;github.com/kubernetes/kubernetes Kubernetes简称为K8s&#xff0c;是用于自动部署、扩缩和管理容器化应用程序的开源系统&#xff0c;起源于Google 集群管理工具Borg。 Kubernetes集群组件逻辑图…

AI核身-金融场景凭证篡改检测Baseline实践

金融领域交互式自证业务中涵盖信用成长、用户开户、商家入驻、职业认证、商户解限等多种应用场景&#xff0c;通常都需要用户提交一定的材料&#xff08;即凭证&#xff09;用于证明资产收入信息、身份信息、所有权信息、交易信息、资质信息等&#xff0c;而凭证的真实性一直是…

柑橘缺陷病害识别数据集YOLO 1290张,xml和txt标签都有;5类别:yolov5-v10通用 包含数据集➕模型➕可视化界面

YOLO柑橘缺陷病害识别数据集 ✓图片数量1290&#xff0c;xml和txt标签都有&#xff1b; 5类 类别&#xff1a;Orange-Black-Spot&#xff0c;Orange-Canker &#xff0c;Orange-Greening&#xff0c;Orange-Healthy&#xff0c;Orange-Melanose&#xff1b; 数据集 YOLO柑橘缺…

微信支付商家转账到零钱审核不通过解决方法

商家转账到零钱功能通常指的是微信支付提供的一项服务&#xff0c;允许商家将资金转账至用户的微信零钱账户。以下是商家转账到零钱的最优申请方案总结&#xff1a; 一、申请条件确认 1. 主体资格&#xff1a; a.申请主体必须为公司性质&#xff08;有限公司类型&#xff09;…

Apache Doris介绍

Apache Doris 的发展 Apache Doris 是一款基于 MPP 架构的高性能、实时的分析型数据库&#xff0c;以高效、简单、统一的特点被人们所熟知&#xff0c;仅需亚秒级响应时间即可返回海量数据下的查询结果&#xff0c;不仅可以支持高并发的点查询场景&#xff0c;也能支持高吞吐的…

【LeetCode每日一题】——724.寻找数组的中心下标

文章目录 一【题目类别】二【题目难度】三【题目编号】四【题目描述】五【题目注意】六【题目示例】七【题目提示】八【解题思路】九【时间频度】十【代码实现】十一【提交结果】 一【题目类别】 前缀和 二【题目难度】 简单 三【题目编号】 724.寻找数组的中心下标 四【…

挖掘空间数据要素典型领域应用场景

空间数据要素作为数字经济的基石&#xff0c;正在多个领域发挥着重要作用。随着技术的发展&#xff0c;空间数据的应用场景不断拓展&#xff0c;为各行各业带来了深刻的变革。以下是几个典型的空间数据要素应用领域&#xff1a; 1. 城市规划与管理 空间数据在城市规划和管理中…

opencv学习:人脸识别器特征提取BPHFaceRecognizer_create算法的使用

BPHFaceRecognizer_create算法 在OpenCV中&#xff0c;cv2.face.LBPHFaceRecognizer_create()函数用于创建一个局部二值模式直方图&#xff08;Local Binary Patterns Histograms&#xff0c;简称LBPH&#xff09;人脸识别器。LBPH是一种用于人脸识别的特征提取方法&#xff0…

Python 入门(二、什么是 Python 的虚拟环境)

Python 入门第二课 &#xff0c;Python 的虚拟环境...... by 矜辰所致前言 本来以为环境搭建好了&#xff0c;就直接开始敲代码了&#xff0c;但是一直看到一个专业词汇&#xff1a;虚拟环境。 对于习惯了嵌入式 C 语言开发博主来说&#xff0c;一开始确实有点不明白&#xf…

k8s杂记

在node节点内部使用kubectl&#xff1a; rootmultinode-demo-m02:/# ps aux | grep kubelet root 218 3.1 1.6 2066316 62516 ? Ssl 07:35 0:29 /var/lib/minikube/binaries/v1.30.0/kubelet --bootstrap-kubeconfig/etc/kubernetes/bootstrap-kubelet.con…

PL/SQL Developer如何连接Oracle数据库(汉化)

简介 PL/SQL Developer是一种用于Oracle数据库开发的集成开发环境&#xff08;IDE&#xff09;。它提供了一个可视化的界面&#xff0c;使开发人员能够方便地编写、调试和执行PL/SQL代码。PL/SQL Developer还具有其他功能&#xff0c;如数据库对象浏览器、SQL编辑器、数据导入…

JavaScript | 定时器(setInterval和clearInterval)的使用

效果图如下&#xff1a; 当用户第一次看到这个页面时&#xff0c;按钮是不可点击的&#xff0c;并显示一个5秒的倒计时。倒计时结束后&#xff0c;按钮变为可点击状态&#xff0c;并显示“同意协议”。这样做的目的是确保用户有足够的时间阅读用户协议。 <!DOCTYPE html>…

AI核身-金融场景凭证篡改检测YOLO原理

引言 YOLO (You Only Look Once) 模型是一种先进的实时目标检测算法&#xff0c;它在计算机视觉领域具有重要的地位。YOLO以其速度和准确性而闻名&#xff0c;能够快速识别图像和视频中的各种物体。这使得它在自动驾驶、安全监控、机器人技术、医学影像分析等众多领域都有着广…

HTML+CSS总结【量大管饱】

文章目录 前言HTML总结语义化标签常用标签H5新的语义元素H5的媒体标签\<embed> 元素&#xff08;少用&#xff09;\<object>元素&#xff08;少用&#xff09;\<audio>\<video> 元素包含关系iframe元素嵌入flash内容常用表单inputselect CSS总结权重样…

【JAVA毕业设计】基于Vue和SpringBoot的渔具租赁系统

本文项目编号 T 005 &#xff0c;文末自助获取源码 \color{red}{T005&#xff0c;文末自助获取源码} T005&#xff0c;文末自助获取源码 目录 一、系统介绍二、演示录屏三、启动教程四、功能截图五、文案资料5.1 选题背景5.2 国内外研究现状5.3 可行性分析 六、核心代码6.1 渔…

Go 语言应用开发:从入门到实战

Go 语言应用开发&#xff1a;从入门到实战 引言 Go&#xff08;Golang&#xff09;是由 Google 开发的一种开源编程语言&#xff0c;设计初衷是提高编程效率&#xff0c;尤其是在高并发场景下表现出色。Go 语言以其简洁、易学、高效并发的特性&#xff0c;逐渐成为开发者的首…

如何将mov格式的视频转换mp4?5种解决方法任你选!

MOV即QuickTime影片格式&#xff0c;它是Apple公司开发的一种音频、视频文件格式&#xff0c;用于存储常用数字媒体类型。然而&#xff0c;它的兼容性主要局限于苹果生态系统。有时&#xff0c;我们需要IOS和Mac设备的视频图片保存到安卓手机或Windows系统中&#xff0c;却发现…

在线matlab环境

登陆https://ww2.mathworks.cn/ 在线文档https://ww2.mathworks.cn/help/index.html 在线环境[需要先登陆]

Java - WebSocket

一、WebSocket 1.1、WebSocket概念 WebSocket是一种协议&#xff0c;用于在Web应用程序和服务器之间建立实时、双向的通信连接。它通过一个单一的TCP连接提供了持久化连接&#xff0c;这使得Web应用程序可以更加实时地传递数据。WebSocket协议最初由W3C开发&#xff0c;并于2…

虚拟机VMware Workstation下CentOS7与主机Windows系统的文件夹共享

虚拟机设置&#xff1a; Linux中安装&#xff1a; yum install open-vm-tools# 判断是否共享&#xff0c;显示共享文件夹则为成功&#xff1a; vmware-hgfsclient# 挂载&#xff1a; vmhgfs-fuse .host:/ /mnt/hgfs/