C++ 多态详解

文章目录

  • 1. 多态的概念
  • 2. 多态的定义及实现
    • 2.1 多态的构成条件
    • 2.2 虚函数
    • 2.3 虚函数的重写
      • 2.3.1 虚函数重写的两个例外
    • 2.4 C++11 override 和 final
    • 2.5 重载、覆盖(重写)、隐藏(重定义)的对比
  • 3. 多态的原理
    • 3.1 虚函数表
    • 3.2多态的原理
  • 4. 单继承和多继承关系的虚函数表
    • 4.1 单继承中的虚函数表
    • 4.2 多继承中的虚函数表

1. 多态的概念

多态是面向对象编程中的一个重要概念,通俗来说,多态就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态
举个例子:比如我们在12306买票,对于买票这个行为,成人买成人票就是全价,学生买学生票就是半价。
在这里插入图片描述

2. 多态的定义及实现

2.1 多态的构成条件

在继承中要构成多态还有两个条件

  1. 必须通过基类的指针或者引用调用虚函数
  2. 被调用的函数必须是虚函数且派生类必须对基类的虚函数进行重写
    在这里插入图片描述
    多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如上面图片中的例子:Student继承了Person。Person对象买票全价,Student对象买票半价。

代码如下:

class Person
{
public:virtual void buy_ticket(){cout << "全价" << endl;}
};
class Student : 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;Student s;//基类的引用func(p);func(s);//基类的指针func(&p);func(&s);return 0;
}

2.2 虚函数

virtual修饰的类成员函数称为虚函数。

class Person
{
public://buy_ticket()就是虚函数,被virtual修饰的类成员函数virtual void buy_ticket(){cout << "全价" << endl;}
};

2.3 虚函数的重写

虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数。
重写虚函数的目的是为了在派生类中提供一个特定于派生类的实现,从而实现特定的行为。重写后,当通过基类指针或引用调用虚函数时,如果指向或引用的是派生类对象,将会调用派生类中的虚函数,如果指向或引用的是基类对象,将会调用基类中的虚函数,这样就可以做到不同的派生类的对象在调用同一个函数时,能表现出不同的行为。
在这里插入图片描述

2.3.1 虚函数重写的两个例外

  1. 协变(基类与派生类虚函数返回值类型不同)
    派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变。
    在这里插入图片描述
  2. 析构函数的重写(基类与派生类析构函数的名字不同)
    如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。
    这里我们可以间接验证一下:
    在这里插入图片描述
    因此在涉及到资源管理的时候,基类的析构函数最好加上virtual关键字修饰,否则可能在某些情况下,造成无法正确调用析构函数而造成内存泄漏。
    比如下面这种情况:
    在这里插入图片描述

2.4 C++11 override 和 final

  1. final:修饰虚函数,表示该虚函数不能再被重写
    在这里插入图片描述
  2. final:修饰类,表示该类不能被继承
    在这里插入图片描述
  3. override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错
    在这里插入图片描述

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

在这里插入图片描述

3. 多态的原理

3.1 虚函数表

我们先来看一道题:

#include<iostream>
using namespace std;
class Base
{
public:virtual void Func1(){cout << "Func1()" << endl;}
private:int _b = 1;
};int main()
{cout << sizeof(Base) << endl;return 0;
}

运行结果:(32位平台下是8,64位平台下位16。)
在这里插入图片描述
我们发现,Base类只有一个整型变量,就算考虑内存对齐,结果应该是4呀,这里为啥会输出8呢?
下面我们打开监视窗口来看下Base类对象的模型:
在这里插入图片描述
通过观察测试我们发现b对象是8bytes,除了_b成员,还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。

针对上面的代码我们做出以下改造
1.我们增加一个派生类Derive去继承Base
2.Derive中重写Func1
3.Base再增加一个虚函数Func2和一个普通函数Func3

#include<iostream>
using namespace std;class Base
{
public:virtual void Func1(){cout << "Base::Func1()" << endl;}virtual void Func2(){cout << "Base::Func2()" << endl;}void Func3(){cout << "Base::Func3()" << endl;}
private:int _b = 1;
};
class Derive : public Base
{
public:virtual void Func1(){cout << "Derive::Func1()" << endl;}
private:int _d = 2;
};
int main()
{Base b;Derive d;return 0;
}

针对改造后的代码,我们接着往下分析派生类中这个表放了些什么呢?
在这里插入图片描述
通过观察和测试,我们发现了以下几点问题:

  1. 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚
    表指针也就是存在部分的另一部分是自己的成员。
    在这里插入图片描述
  2. 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表
    中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数
    的覆盖
    。重写是语法的叫法,覆盖是原理层的叫法。
  3. 另Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函
    数,所以不会放进虚表。
  4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。
    在这里插入图片描述
  5. 总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生
    类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己
    新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
  6. 虚函数存在哪的?虚表存在哪的?
    答:虚函数存在虚表,虚表存在对象中。注意上面的回答的错的。注意虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。那么虚表存在哪的呢?
    在这里插入图片描述
    上面分析了这个半天了那么多态的原理到底是什么?下面我们来具体分析一下:

3.2多态的原理

现在我们再来看下之前写的买票的代码,Func函数传Person调用的Person::BuyTicket,传Student调用的是Student::BuyTicket:

#include<iostream>
using namespace std;
class Person 
{
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person 
{
public:virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
void Func(Person& p)
{p.BuyTicket();
}
int main()
{Person p;Func(p);Student s;Func(s);return 0;
}

在这里插入图片描述
上面我们分析出,当p指向谁就去谁的虚函数表中去取对应虚函数的地址,因此就实现出了不同对象完成同一行为时,展现出不同的形态。
而我们要达到多态,有两个条件,一个是虚函数覆盖,一个是对象的指针或引用调用虚函数。这是为什么呢?我们来反思一下:
在这里插入图片描述
我们分析出,赋值不会拷贝虚函数指针,因此要实现出不同对象完成同一行为时,展现出不同的形态,有两个条件,一个是虚函数覆盖,一个是对象的指针或引用调用虚函数。
下面我们在来分析一下多态调用和普通函数调用有什么区别呢?
在这里插入图片描述
通过上面的汇编代码分析,看出满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中取找的。不满足多态的函数调用时编译链接时确认好的

4. 单继承和多继承关系的虚函数表

在单继承和多继承关系中,下面我们去关注的是派生类对象的虚表模型,因为基类
的虚表模型前面我们已经看过了,没什么需要特别研究的。

4.1 单继承中的虚函数表

分析如下代码:

class Base 
{ 
public :virtual void func1() { cout<<"Base::func1" <<endl;}virtual void func2() {cout<<"Base::func2" <<endl;}
private :int a;
};
class Derive :public Base 
{ 
public :virtual void func1() {cout<<"Derive::func1" <<endl;}virtual void func3() {cout<<"Derive::func3" <<endl;}virtual void func4() {cout<<"Derive::func4" <<endl;}
private :int b;
};int main()
{Base b;Derive d;return 0;
}

分析如下:
在这里插入图片描述
我们通过监视窗口中我们发现看不见func3和func4。这里是编译器的监视窗口故意隐藏了这两个函数,也可以认为是他的一个小bug。而且通过内存窗口只能看清虚函数的个数,那么我们如何查看d的虚表呢?下面我们使用代码打印出虚表中的函数。

class Base
{
public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }
private:int a;
};
class Derive :public Base
{
public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }virtual void func4() { cout << "Derive::func4" << endl; }
private:int b;
};
typedef void (*VFPTR)();
void PrintVFTable(VFPTR* vftptr)
{for (int i = 0; vftptr[i] != 0; ++i){printf("第%d个虚函数的地址:%p------>", i + 1, vftptr[i]);vftptr[i]();}printf("\n");
}
int main()
{Base b;Derive d;
//思路:取出b、d对象的头4bytes,就是虚表的指针,前面我们说了虚函数表本质是一个存虚函数指针的指针数组,
// 这个数组最后面放了一个nullptr
// 1.先取b的地址,强转成一个int*的指针
// 2.再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针
// 3.再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。
// 4.虚表指针传递给PrintVTable进行打印虚表VFPTR* vftptr = (VFPTR*)(*(int*)&b);PrintVFTable(vftptr);vftptr = (VFPTR*)(*(int*)&d);PrintVFTable(vftptr);return 0;
}

运行结果如下:
在这里插入图片描述
因此我们可以推断出派生类的虚函数表模型:
在这里插入图片描述

4.2 多继承中的虚函数表

分析如下代码:

#include <iostream>
using namespace std;class Base1 
{
public:virtual void func1() { cout << "Base1::func1" << endl; }virtual void func2() { cout << "Base1::func2" << endl; }
private:int b1 = 1;
};
class Base2 {
public:virtual void func1() { cout << "Base2::func1" << endl; }virtual void func2() { cout << "Base2::func2" << endl; }
private:int b2 = 2;
};
class Derive : public Base1, public Base2 
{
public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }
private:int d1 = 0;
};
typedef void(*VFPTR) ();
void PrintVFTable(VFPTR* vftptr)
{for (int i = 0; vftptr[i] != 0; ++i){printf("第%d个虚函数的地址:%p------>", i + 1, vftptr[i]);vftptr[i]();}printf("\n");
}
int main()
{Derive d;VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);PrintVFTable(vTableb1);VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d + sizeof(Base1)));PrintVFTable(vTableb2);return 0;
}

通过监视创窗口看下d对象的模型:
在这里插入图片描述
通过监视窗口可以看出,d对象有两个虚函数指针和从两个基类继承下来的成员以及自己的成员,但是从监视窗口无法看出两个虚函数表具体放了哪几个虚函数,因此下面我们打印一下两个虚函数表:
观察下图可以看出:多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中!!!
在这里插入图片描述

从上图中可以看出,两个虚函数表中的func1是同一个函数,但是两张表中的地址不一样这是为啥呢?
在这里插入图片描述
下面从汇编代码看下,这个函数是如何调用的:
在这里插入图片描述
从上表中看出,两个表中func1的地址不同,是因为第二个虚函数表中的func1在调用的时候要修正this指针。

至此,本片文章就结束了,若本篇内容对您有所帮助,请三连点赞,关注,收藏支持下。

创作不易,白嫖不好,各位的支持和认可,就是我创作的最大动力,我们下篇文章见!

如果本篇博客有任何错误,请批评指教,不胜感激 !!!
在这里插入图片描述

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

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

相关文章

docker安装【zookeeper】【kafka】【provectuslabs/kafka-ui】记录

目录 1.安装zookeeper:3.9.2-jre-172.安装kafka:3.7.03.安装provectuslabs/kafka-ui &#xff08;选做&#xff09;新环境没有jdk&#xff0c;安装jdk-17.0.10备用 mkdir -p /export/{data,apps,logs,conf,downloads}cd /export/downloadscurl -OLk https://download.oracle.…

新品发布!无人机装调检修实训系统

近年&#xff0c;我国密集出台相关产业政策&#xff0c;推动低空经济从探索走向发展&#xff0c;根据新华网数据&#xff0c;2030年低空经济规模有望达2万亿。无人机专业属于跨学科的综合性专业&#xff0c;其中装调检测技术是无人机教培的重要组成部分。 天途推出无人机装调检…

Apache SeaTunnel k8s 集群模式 Zeta 引擎部署指南

SeaTunnel提供了一种运行Zeta引擎(cluster-mode)的方法&#xff0c;可以让Kubernetes在本地运行Zeta引擎&#xff0c;实现更高效的应用程序部署和管理。在本文中&#xff0c;我们将探索SeaTunnel k8s运行zeta引擎(cluster-mode模式)的更多信息&#xff0c;了解如何更好地利用Ze…

HTML:元素分类

HTML&#xff1a;元素分类 概述块级元素&#xff08;Block-level Elements&#xff09;内联元素&#xff08;Inline Elements&#xff09;替换元素&#xff08;Replaced Elements&#xff09;表单元素&#xff08;Form Elements&#xff09; 概述 HTML&#xff08;HyperText M…

Docker容器:网络模式与资源控制

目录 一、Docker 网络模式 1、Docker 网络实现原理 2、Docker 网络模式概述 2.1 Host 模式 2.2 Container 模式 2.3 None 模式 2.4 Bridge 模式 2.5 自定义网络&#xff08;user-defined network&#xff09; 3、配置 docker 网络模式 3.1 查看网络基础命令 3.1.1 查…

css利用transform:skew()属性画一个大屏的背景斜面四边形特效

在工作工程中需要写一个如下的大屏背景&#xff0c;是由几个斜面做成的效果 使用css transform function中的skew()方法实现画其中一个斜面&#xff0c;然后调整背景色实现 写一个div <div class"skew_container test-2"><div class"skew_container_it…

【python笔记】datafram的时间动态可视化 pyecharts地图

import pandas as pd# 假设DataFrame是这样的&#xff1a; df pd.DataFrame({ year: [2014, 2015, 2016, 2014, 2015, 2016, 2014, 2015, 2016], province: [广东省, 广东省, 河南省, 湖南省, 北京市, 北京市, 上海市, 新疆维吾尔自治区, 上海市], values: [100, 150, 75…

Servlet文件嵌套<script>来显示提示框而出现乱码的解决方案

主要出现的原因就是编码不统一导致无法解析对话框中的字符串 我的解决方案: 使用 URL 的编码格式&#xff0c;然后再使用js中的decodeURIComponent函数解析URL编码的字符串&#xff0c;并且恢复其原始字符串内容 将你写的传统的编码格式改为 PrintWriter out resp.getWriter(…

多目标应用:MSSA多目标樽海鞘优化算法求解无人机三维路径规划(MATLAB代码)

一、无人机多目标优化模型 无人机三维路径规划是无人机在执行任务过程中的非常关键的环节&#xff0c;无人机三维路径规划的主要目的是在满足任务需求和自主飞行约束的基础上&#xff0c;计算出发点和目标点之间的最佳航路。 1.1路径成本 无人机三维路径规划的首要目标是寻找…

YOLOV8 pycharm

1 下载pycharm 社区版 https://www.jetbrains.com/zh-cn/pycharm/download/?sectionwindows 2 安装 3 新建 4 选择 文件-> setting 配置环境变量 5 添加conda 环境

基本STL使用

一 、关于vector 在STL中有一个称为vector的数据结构&#xff0c;可以用来代替数组。 定义Book特性 private:vector<string> shelf_books;Notic : 类中不能使用类似的定义&#xff1a;vector<sttring> shelf_boos( 10 ); 定义Book方法 public:void setName(str…

5.C++动态内存管理(超全)

目录 1 .C/C 内存分布 2. C语言中动态内存管理方式&#xff1a;malloc/calloc/realloc/free 3. C内存管理方式 3.1 new/delete操作内置类型 3.2 new和delete操作自定义类型 3.3 operator new函数 3.4 定位new表达式(placement-new) &#xff08;了解&#xff09; 4. 常…

Java 基础重点知识-(Java 语言特性、数据类型、常见类、异常)

文章目录 Java 语言特性形参和实参的区别是什么?值传递和引用传递的区别?Java 是值传递还是引用传递?final 的作用是什么?final finally finalize 有什么不同?static 的作用是什么?static 和 final 的区别是什么? Java 数据类型Java基本数据类型有几种? 各占多少位?基…

【MySQL精炼宝库】数据库的约束 | 表的设计 | 聚合查询 | 联合查询

目录 一、数据库约束 1.1 约束类型&#xff1a; 1.2 案例演示&#xff1a; 二、表的设计 2.1 一对一: 2.2 一对多: 2.3 多对多: 2.4 内容小结&#xff1a; 三、新增 四、查询 4.1 聚合查询&#xff1a; 4.1.1 聚合函数&#xff1a; 4.1.2 GROUP BY子句&#xff1a…

windows驱动开发-中断(一)

中断是windows中最难的一部分&#xff0c;这是因为中断本身属于操作系统的一部分&#xff0c;理解了中断和内存&#xff0c;对整个系统也就了解了。 中断部分会先从中断优先级、中断处理、中断服务例程入手&#xff0c;大概讲述一下中断的概念&#xff1b;接着从中断的一般实现…

C语言:指针详解(3)

目录 一、字符指针 二、数组指针 1.数组指针的定义 2.数组指针的初始化 3. 二维数组传参的本质 三、函数指针 1.函数指针的创建 2.函数指针的使用 3.有趣的代码(1) 4.有趣的代码(2) 四、typedef关键字 1.typedef的使用方法 2.typedef和#define的区别 五、函数指针…

前端性能优化知识梳理

1.重要性 当我们面试的时候&#xff0c;前端性能优化方面算是必考的知识点&#xff0c;但是工作中我们又很少会重点的对项目进行前端优化&#xff0c;它真的不重要吗&#xff1f; 如果我们可以将后端响应时间缩短一半&#xff0c;整体响应时间只能减少5%~10%。而如果关注前端…

imx6ull启动方式和镜像文件烧写

文章目录 前言一、BOOT启动方式1.串行下载2.内部BOOT模式 二、内部BOOT模式详细流程1.启动设备的选择2.镜像烧写 总结 前言 &#x1f4a6; I.MX6Ull 支持多种启动方式以及启动设备&#xff0c;比如可以从 SD/EMMC、NAND Flash、QSPI Flash等启动。用户可以根据实际情况&#x…

【web安全】-- 命令执行漏洞详解

本文将从原理开始介绍命令执行漏洞并附有三个实例来供各位客官学习 文章目录 一、什么是命令执行漏洞二、出现的原因三、有可能存在命令执行漏洞的函数&#xff08;php&#xff09;1、利用一些函数来实现命令执行2、直接执行系统命令的函数 四、命令拼接符号1、Windows2、linux…

QT:不同UI间数据,信号的交互

前言 接上文&#xff0c;手动绘制的矩形框毕竟还是在上位机的播放界面内&#xff0c;想要把数据发送给3559还是需要通过串口或者网口发送&#xff0c;没有部署在一个界面就需要不同UI间数据和信号进行交互了&#xff0c;数据还好说&#xff0c;全局变量都可以做到&#xff0c;信…