【C++笔记】多态的原理、单继承和多继承关系的虚函数表、 override 和 final、抽象类、重载、覆盖(重写)、隐藏(重定义)的对比

1.final关键字

引出:设计一个不能被继承的类。有如下方法:

class A
{
private:A(int a=0):_a(a){}
public:static A CreateOBj(int a=0){return A(a);}
protected:int _a;
}
//简介限制,子类构成函数无法调用父类构造函数初始化
//子类的构造函数一定去调用父类的构造函数去初始化那一部分
//而父类的构造函数继承下来后为不可见
class B:public A
{}
int main()
{A aa=;//构造函数被私有化,报错//可以通过如下方法进行A类对象的创建A aa=A::CreateOBj(10);
}

以上的方法虽然可以实现我们的需求,但是会对类进行强制性的封装,所有我们在C++11中还有更好的方法,通过final关键字来实现。

  1. C++11中final声明表示为最终类,表明该类不能被继承。
  2. 修饰函数,限制函数不能被重写。
//修饰类
class A final
{
protected:int _a;
};
//直接限制报错,A不可被继承
class B:public A
{};
//修饰函数
class C
{
public:virtual void f() final{cout<<"C::f()"<<endl;}
}
class D:public C
{
public://直接报错virtual void f(){}
}

2. override

override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。

class Car{
public:virtual void Drive(){}};class Benz :public Car {public:virtual void Drive() override {cout << "Benz-舒适" << endl;}};

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

  • 重载:
    1、两个函数在同一作用域
    2、函数名/参数相同
  • 重写(覆盖):
    1、两个函数分别在基类和派生类的作用域
    2、函数名/参数/返回值都必须相同(协变例外)
    3、两个函数必须是虚函数
  • 重定义(隐藏):
    1、两个函数分别在基类和派生类的作用域
    2、函数名相同
    3、两个基类和派生类的同名函数不构成重写就是重定义

4.纯虚函数

在虚函数的后面写上 =0 ,则这个函数为纯虚函数。

class Student
{
public:virtual void show()=0;
};

5.抽象类

5.1概念

包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承:本质上强制子类去完成虚函数的重写。

 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;}};

5.2接口继承和实现继承

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。

6.多态的原理

6.1认识虚函数表

先来看一道经典的问题,sizeof(Base)是多少?

class Base{public:virtual void Func1(){cout << "Func1()" << endl;}private:int _b = 1;};

很多一部分的答案是4Byte,但实际正确答案是8Byte,这是怎么一回事呢?看Visual Studion下的监视窗口。如下图。
图1
结论:
1.除了_b成员,还多一个__vfptr放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。
2.一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表。
继续深入:如果一个类的继承了该类,那么派生类中这个表放了些什么呢?

// 针对上面的代码我们做出以下改造
// 1.我们增加一个派生类Derive去继承Base
// 2.Derive中重写Func1
// 3.Base再增加一个虚函数Func2和一个普通函数Func3class 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;}

看Visual Studion下的监视窗口。如下图。
在这里插入图片描述
通过对监视窗口的分析可以得出如下结论:

  1. 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在的部分,另一部分是自己的成员
  2. 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
  3. 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表。
  4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。
  5. 总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
  6. 这里还有一个很容易混淆的问题:虚函数存在哪的?虚表存在哪的? 答:虚函数存在虚表,虚表存在对象中。注意上面的回答的错的。 但是很多同学都是这样深以为然的。注意:虚函数表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚函数表中。另外对象中存的不是虚函数表,存的是虚函数表指针。那么虚表存在哪的呢?代码段! 感兴趣的同学可以用如下代码去打印一下虚表的地址,来验证一下。
typedef void(*VF_PTR)();//void PrintVFTable(VF_PTR table[])
// 打印虚函数表中内容
void PrintVFTable(VF_PTR* table)
{for (int i = 0; table[i] != nullptr; ++i){printf("vft[%d]:%p->", i, table[i]);VF_PTR f = table[i];f();}cout << endl << endl;
}
int main()
{Base b;PrintVFTable((VF_PTR*)(*(void**)&b));Derive d;PrintVFTable((VF_PTR*)(*(void**)&d));return 0;
}

7.在函数调用过程中,对象先找到虚函数表指针,通过该指针找到虚函数表,再在虚函数表中找到对应的函数指针,通过函数指针找到所需要的哪个函数。如下图所示。
在这里插入图片描述

6.2多态的原理

 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 Mike;Func(&Mike);Student Johnson;Func(&Johnson);return 0;}

在这里插入图片描述

  1. 观察上图的红色箭头我们看到,p是指向mike对象时,p->BuyTicket在mike的虚表中找到虚函数是Person::BuyTicket。
  2. 观察上图的蓝色箭头我们看到,p是指向johnson对象时,p->BuyTicket在johson的虚表中找到虚函数是Student::BuyTicket。
  3. 这样就实现出了不同对象去完成同一行为时,展现出不同的形态。
  4. 反过来思考我们要达到多态,有两个条件,一个是虚函数覆盖,一个是对象的指针或引用调用虚函数。反思一下为什么?单纯的对象的话所有的对象的虚函数表都是相同的,利用指针或者引用可以看作指向对象当中父类那一部分的成员或者指针。覆盖使其不同的虚表中,指向的虚函数不同,从而完成多种状态
  5. 看出满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中取找的。不满足多态的函数调用时编译时确认好的。
    !!!一图搞懂多态的原理
    在这里插入图片描述

7.多继承中的虚函数表

 class Base1 {public:virtual void func1() {cout << "Base1::func1" << endl;}virtual void func2() {cout << "Base1::func2" << endl;}private:int b1;};class Base2 {public:virtual void func1() {cout << "Base2::func1" << endl;}virtual void func2() {cout << "Base2::func2" << endl;}private:int b2;};class Derive : public Base1, public Base2 {
public:virtual void func1() {cout << "Derive::func1" << endl;}virtual void func3() {cout << "Derive::func3" << endl;}
private:int d1;};typedef void(*VFPTR) ();void PrintVTable(VFPTR vTable[]){cout << " 虚表地址>" << vTable << endl;for (int i = 0; vTable[i] != nullptr; ++i){printf(" 第%d个虚函数地址 :0X%x,->", i, vTable[i]);VFPTR f = vTable[i];f();}cout << endl;}int main(){Derive d;VFPTR* vTableb1 = (VFPTR*)(*(int*)&d);PrintVTable(vTableb1);VFPTR* vTableb2 = (VFPTR*)(*(int*)((char*)&d+sizeof(Base1)));PrintVTable(vTableb2);return 0;}

在这里插入图片描述
分析上述图片可以得出如下结论:
1、多继承派生类的未重写的虚函数放在第一个继承基类部分的虚函数表中
2、多继承时,子类重写了Base1和Base2虚函数func1,但是虚函数表中重写的func1的地址却不一样,但是没关系,他们最终还是调到同一个函数。这是因为存在偏移量的问题。

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

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

相关文章

IDEA如何拉取gitee项目?

1.登录gitee 说明&#xff1a;打开idea&#xff0c;在设置上面搜索框输入gitee&#xff0c;然后登录gitee注册的账号。 2. 创建gitee仓库 说明&#xff1a;创建idea中的gitee仓库。 3.寻找项目文件 说明&#xff1a;为需要添加gitee仓库的项目进行添加。 4.项目右键 说明&a…

IPIDEA代理IP如何帮助企业采集市场信息

在当今数字化的时代&#xff0c;市场信息对于企业的发展至关重要。然而&#xff0c;如何高效地收集市场信息成为了每个企业都需要面对的问题。爬虫技术的出现为企业提供了一种高效、便捷的信息采集方式。然而&#xff0c;由于爬虫的请求频率较高&#xff0c;目标网站可能会将频…

vue使用pdf-dist实现pdf预览以及水印

vue使用pdf-dist实现pdf预览以及水印 一.使用pdf-dist插件将PDF文件转换为一张张canvas图片 npm install pdf-dist二.页面引入插件 const pdfJS require("pdfjs-dist"); pdfJS.GlobalWorkerOptions.workerSrc require("pdfjs-dist/build/pdf.worker.entry&…

浅谈云原生

目录 1. 云原生是什么&#xff1f; 2. 云原生四要素 2.1 微服务 2.2 容器化 2.3 DevOps 2.4 持续交付 3. 具体的云原生技术有哪些&#xff1f; 3.1 容器 (Containers) 3.2 微服务 (Microservices) 3.3 服务网格 (Service Meshes) 3.4 不可变基础设施 (Immutable Inf…

支持PC端、手机端、数据大屏端的Spring Cloud智慧工地云平台源码

技术架构&#xff1a;微服务JavaSpring Cloud VueUniApp MySql 智慧建筑工地云平台主要利用大数据、物联网等技术&#xff0c;整合工地信息、材料信息、工程进度等&#xff0c;实现对建筑项目的全程管理。它可以实现实时监测和控制&#xff0c;有效解决施工中的问题&#xff0c…

软件考试学习笔记(希赛)

软考学习笔记-软件设计师 1. 软考基本介绍1.1 软考分数制1.2软考考试分类介绍1.3软件考试报名网站1.4考试内容1.4.1上午考试内容-综合知识1.4.2下午考试内容-软件设计 2.数据的表示2.1进制转换2.1.1R进制------》十进制转换2.1.2十进制-----》R进制转换2.1.3二进制与八进制与16…

网络通信协议-HTTP、WebSocket、MQTT的比较与应用

在今天的数字化世界中&#xff0c;各种通信协议起着关键的作用&#xff0c;以确保信息的传递和交换。HTTP、WebSocket 和 MQTT 是三种常用的网络通信协议&#xff0c;它们各自适用于不同的应用场景。本文将比较这三种协议&#xff0c;并探讨它们的主要应用领域。 HTTP&#xff…

Gazebo仿真 【ROS: noetic】

参考链接&#xff1a;《ROS机器人开发实践》_胡春旭 目标&#xff1a; 了解如何使用URDF文件创建一个机器人模型&#xff0c;然后使用xacro文件优化该模型&#xff0c;并且放置到rvizArbotiX或Gazebo仿真环境中&#xff0c;以实现丰富的ROS功能。 4.5 Gazebo仿真环境 1&#x…

《动手学深度学习 Pytorch版》 9.1 门控循环单元(GRU)

我们可能会遇到这样的情况&#xff1a; 早期观测值对预测所有未来观测值具有非常重要的意义。 考虑一个极端情况&#xff0c;其中第一个观测值包含一个校验和&#xff0c;目标是在序列的末尾辨别校验和是否正确。在这种情况下&#xff0c;第一个词元的影响至关重要。我们希望有…

PS修改背景色,线框底图

1、打开图片&#xff0c;ctrlj复制一层 2、图像-调整-反相 3、ctrll调整色阶&#xff0c;将中间的色块向右移&#xff0c;灰色线和字体的会变黑

游戏类app有哪些变现方式?

游戏类app有多变现策略&#xff0c;一些是一些主要的方式&#xff1a;#APP广告变现# AdSet官方资讯-上海神蓍信息科技有限公司 一、游戏销售 一次性购买&#xff1a;玩家支付一次性费用购买游戏&#xff0c;之后可以免费游玩。这种模式常见于主机游戏和PC游戏。 游戏包&…

VR数字政务为我们带来了哪些便捷之处?

每每在政务大厅排队的时候&#xff0c;总是在想未来政务服务会变成什么样子呢&#xff1f;会不会变得更加便捷呢&#xff1f;今天我们就来看看VR数字政务&#xff0c;能够为我们带来哪些便捷之处吧&#xff01; 传统的政务服务中&#xff0c;不仅办事流程复杂&#xff0c;而且每…

单链表的相关操作(初阶--寥寥万字不成敬意)

目录 链表的概念 链表的相关操作&#xff1a; 链表的创建&#xff1a; 打印链表&#xff1a; 申请新节点&#xff1a; 链表的尾插&#xff1a; &#xff01;&#xff01;&#xff01;对于传参中二级指针的解释&#xff1a; 链表的头插&#xff1a; 链表的尾删&#xff…

保护隐私就是在保护自己!如何在Android上更改应用程序权限

如果你关心隐私&#xff0c;知道如何在Android上更改应用程序权限将成为一项非常重要的技能。即使是最好的安卓应用程序也可以对手机的功能和数据进行广泛的访问&#xff0c;因此准确控制它们的使用范围会有所帮助。 一旦你在手机上加载了应用程序&#xff0c;你可能会注意到它…

【LeetCode】35. 搜索插入位置

1 问题 给定一个排序数组和一个目标值&#xff0c;在数组中找到目标值&#xff0c;并返回其索引。如果目标值不存在于数组中&#xff0c;返回它将会被按顺序插入的位置。 请必须使用时间复杂度为 O(log n) 的算法。 示例 1: 输入: nums [1,3,5,6], target 5 输出: 2 示例…

在线课堂知识系统源码系统+前端+后端完整搭建教程

大家好啊&#xff0c;今天罗峰来给大家分享一款在线课堂知识系统源码系统。这款系统的功能十分强大。可以使用手机随时随地地学习&#xff0c;有专业的导师答疑解惑。支持视频&#xff0c;音频&#xff0c;图文章节。以下是部分核心代码图&#xff1a; 系统特色功能一览&#x…

Linux上Docker的安装以及作为非运维人员应当掌握哪些Docker命令

目录 前言 1、安装步骤 2、理解镜像和容器究竟是什么意思 2.1、为什么我们要知道什么是镜像&#xff0c;什么是容器&#xff1f; 2.2、什么是镜像&#xff1f; 2.3、什么是容器&#xff1f; 2.4、Docker在做什么&#xff1f; 2.5、什么是镜像仓库&#xff1f; 2、Dock…

AN动画基础——缓动动画

【AN动画基础——影片剪辑滤镜】 基础动画缓动动画缓动原理实例应用 本篇内容&#xff1a;了解曲线原理 重点内容&#xff1a;缓动动画 工 具&#xff1a;Adobe Animate 2022 基础动画 我们先做一个非缓动的效果的动画。 绘制一个矩形设置成元件—图形&#xff0c;30帧插入关…

论文阅读 Memory Enhanced Global-Local Aggregation for Video Object Detection

Memory Enhanced Global-Local Aggregation for Video Object Detection Abstract 人类如何识别视频中的物体&#xff1f;由于单一帧的质量低下&#xff0c;仅仅利用一帧图像内的信息可能很难让人们在这一帧中识别被遮挡的物体。我们认为人们识别视频中的物体有两个重要线索&…

C# 取消一个不带CancellationToken的任务?

在异步函数中&#xff0c;一般使用CancellationToken来控制函数的执行。这个Token需要作为参数传递到异步函数中&#xff1a; public staic Task<T> DoAsync(CancellationToken token) {... } 那么如果一个异步函数没有这个Token参数&#xff0c;如何取消呢? 之前看到一…