C++:多态

目录

一.多态的概念

二.多态的定义及其实现

1.虚函数

2.虚函数的重写/覆盖

3.实现多态的条件

 4.虚函数重写的例外

5.析构函数的重写

6.经典例题

7.C++11 override和final关键字

8.重载、重写/覆盖、隐藏的区别

三.抽象类

四.多态的原理

1.虚函数表指针

2.多态如何实现

3.动态绑定与静态绑定

4.虚函数表

5.有关虚函数和虚函数表的存储位置

五.常见问题


一.多态的概念

多态(polymorphism):简单来说,就是多种形态。

多态可以分为编译时多态(静态多态)运行时多态(动态多态)。

编译时多态主要就是函数重载函数模板,传递不同类型的参数调用不同的函数,通过参数不同达到多种形态。至于叫做编译时多态的原因,是它们实参传给形参的参数匹配是在编译时完成的。

我们主要介绍运行时多态。

运行时多态,就是具体完成某个行为(函数),传递不同的对象就会完成不同的行为,从而达到多种形态。例如买票这个行为,普通人买票就是全价;学生买票就是优惠价;军人买票就是优先买票。再或者,对于一个动物叫的行为(函数),传猫对象过去就是喵喵喵,传狗对象过去,就是汪汪汪。

二.多态的定义及其实现

1.虚函数

虚函数就是类成员函数前加virtual修饰。注意非成员函数不能加virtual修饰

class Person
{
public:virtual void Buyticket(){cout << "全价买票" << endl;}
};

2.虚函数的重写/覆盖

虚函数的重写/覆盖:子类中有一个跟父类完全相同的虚函数,这里的”完全相同”指的是两个虚函数的返回类型、函数名、参数列表(参数类型与个数)完全相同,这样则称子类的虚函数重写了父类的虚函数。

注意:虚函数的重写是对函数实现部分的重写。

class Person
{
public:virtual void BuyTicket(){cout << "全价买票" << endl;}
};class Student : public Person {
public:virtual void BuyTicket(){cout  << "打折买票" << endl;}
};

子类Student的BuyTicket函数重写了父类Person的BuyTicket函数。

注意:在重写父类虚函数时,子类的虚函数可以不加virtual关键字,这样也构成重写。能这样做的原因在于父类的虚函数被子类继承下来后依旧保留了虚函数的属性,但是这种写法不规范,不建议写,不过考试会埋这样的坑。

3.实现多态的条件

多态的实现效果:

class Person
{
public:virtual void BuyTicket(){cout << "全价买票" << endl;}
};class Student : public Person {
public:void BuyTicket(){cout  << "打折买票" << endl;}
};void Func(Person* ptr)
{ptr->BuyTicket();
}int main()
{Person ps;Student st;Func(&ps);Func(&st);return 0;
}

 

 想要实现多态必须满足两个条件。

  • 必须是父类的指针或者引用。

因为只有是父类的指针或者引用才可以既能指向父类对象,又能指向子类对象

简单来说,在传递不同对象时,只有父类的指针或者引用才能接收子类对象或者父类对象。

而父类指针或者引用有这样的功能是通过子类切片去实现的。

  • 子类必须对父类的虚函数进行重写/覆盖。

 4.虚函数重写的例外

子类重写父类虚函数时,子类虚函数返回类型可以与父类虚函数返回类型不同,称为协变。

这里类型不同指的是,父类虚函数返回父类对象的指针或者引用子类虚函数返回子类对象的指针或者引用

class A {};
class B : public A{};class Person
{
public:virtual A* BuyTicket(){cout << "全价买票" << endl;return nullptr;}
};class Student : public Person {
public:virtual B* BuyTicket(){cout  << "打折买票" << endl;return nullptr;}
};void Func(Person* ptr)
{ptr->BuyTicket();
}int main()
{Person ps;Student st;Func(&ps);Func(&st);return 0;
}

这里的A *可以替换成Person*,B*可以替换成Student*。

5.析构函数的重写

我们首先要了解析构函数的名字看起来不同,但实际上会被编译器统一处理成destructor,所以析构函数的名字实际上都是destructor

 当父类的析构函数为虚函数,此时子类析构函数只要定义,无论加不加virtual关键字,都与父类的析构函数构函数构成重写。原因就在于析构函数的名字实际上都是一样的。

我们来观察下面这段代码。

class A
{
public:virtual ~A(){cout << "~A" << endl;}
};class B : public A
{
public:~B(){cout << "~B->delete:" << _ptr << endl;delete[] _ptr;}
protected:int* _ptr = new int[20];
};int main()
{A* p1 = new A;A* p2 = new B;delete p1;delete p2;return 0;
}

(delete的原理类似于先调用各自的析构函数,再去free)

这里调用析构函数实现了多态的效果,只有实现多态,才能正确调用析构函数,父类A调用~A(),

子类B调用~B(),如果不正确调用析构函数,就会有资源泄漏的风险。

这里想要实现多态,就要对子类的析构函数进行重写,想要进行析构函数的重写,那析构函数的名字就必须被编译器统一处理成destructor来达成重写的条件。

这也回答了为什么父类的析构函数建议设计成虚函数,如果不设计成虚函数,那又谈何虚函数的重写,也就无法实现析构函数的多态。

6.经典例题

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

以上程序输出结果是什么()

A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确

这里子类B中的func函数重写了父类A中的func函数。

首先B*类型的指针p调用了继承父类B的test()函数,test()函数是有个A*类型隐式this指针,这里就是父类A的指针接收了子类B对象。test()函数又调用了func函数,func函数满足虚函数的重写,这里就达成了多态实现的条件,所以应该调用的是B类中的func函数,打印B->0。

但事实上,这是大错特错的,这道题坑就坑在虚函数的重写是重写函数的实现部分。子类B继承父类A的接口声明( virtual void func(int va1=1) ),然后重写函数的实现部分({ std::cout << "B->" << val << std::endl; }),重写后的func函数变成了下面这样,缺省值用的是1,打印B->1。

virtual void func(int val = 1)
{cout << "B->" << val << endl;
}

打个不恰当的比喻,虚函数的重写就是把一个人的头(函数声明)接到了另一个人的身子上(函数实现)。 

7.C++11 override和final关键字

1.override

只能修饰子类的虚函数(放在函数参数列表的后面),用于检测该虚函数是否完成重写,如果没有,则报错(编译错误)。

2.final

如果不想让子类重写父类虚函数,则我们用final修饰。

final用于修饰父类的虚函数。

如果final修饰了一个类,那这个类就不能被其他类继承。

8.重载、重写/覆盖、隐藏的区别

三.抽象类

在虚函数后面加上 =0,这个虚函数就是纯虚函数。纯虚函数不需要定义只要需要声明就行了。

纯虚函数也是实现具体的函数部分,但是没有意义,因为纯虚函数实现部分会被重写,父类不能实例出对象,也无法调用纯虚函数。(说纯虚函数不能定义是错误的)

含有纯虚函数的类叫做抽象类,抽象类不能实例化出对象。当子类继承抽象类后不重写虚函数那么子类也是抽象类。所以纯虚函数在某种程度上强制要求子类重写虚函数,因为不重写子类实例不出具体的对象。

四.多态的原理

1.虚函数表指针

我们先来看一道题

下面编译为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;
}

按照我们以前所学的知识,b的大小应该是8字节,但实际上程序运行的结果是12字节。

Base类除了有_b和_ch成员,还有一个虚函数表指针_vfptr放在前面(有些平台可能会放到后面,这跟平台有关)。这个_vfptr指针,v代表virtual,f代表function。所以结果应该是8+4=12字节。

一个含有虚函数的类中至少有一个虚函数表指针,一个类所有的虚函数的地址都要放到这个类对象的虚函数表中,虚函数表也叫做虚表。

2.多态如何实现

我们以刚才买票的例子来说明多态是如何实现的。

通过下图我们可以看到,满足多态条件后,底层不再是编译时通过调用对象确定函数的地址而是运行时到指向的对象的虚函数表中确定对应的虚函数的地址,这样就是实现了父类指针或引用指向父类就调用父类的虚函数,指向子类就调用子类对应的虚函数。

由上图我们知道,虽然都是Person类型的指针Ptr在调用BuyTicket,但是跟Ptr的类型无关,调用的BuyTicket函数跟Ptr指向的类型有关,Ptr指向Person类对象调用Person的BuyTicket(全价买票),Ptr指向Student类对象调用Student的BuyTicket(打折买票)。

3.动态绑定与静态绑定

对不满足多态条件(指针+引用调用虚函数)的函数调用是在编译时绑定调用函数的地址,叫做静态绑定

满足多态条件的函数调用是在运行时绑定函数调用的地址,也就是运行时到指向对象的虚函数表中找到调用函数的地址,叫做动态绑定。

我们从汇编层面观察两者的区别。

动态绑定,编译在运行到ptr指向对象的虚函数表中确定函数地址。

静态绑定,编译器直接确定调用函数地址。

4.虚函数表

下面我们来详细介绍虚函数表。

  1. 同一类类型的对象共享同一张虚函数表,不同类类型的对象的虚函数表则不一样,具体点就是不同类对象的虚函数表指针不一样。
  2. 基类对象的虚函数表中存放基类所有的虚函数的地址。
  3. 首先派生类由两部分构成,继承下来的的基类和自己的成员,一般情况下,如果继承下来的基类有虚函数表指针,那派生类就不会生成虚函数表指针。但要注意继承下来的基类部分虚函数表指针和基类对象的虚函数表指针不是同一个,就像基类对象的成员和派生类对象中的基类对象成员也独立存在一样。
  4. 派生类重写的基类的虚函数派生类的虚函数表中对应的虚函数被覆盖成派生类重写的虚函数地址
  5. 派生类的虚函数表包含,基类的虚函数地址、派生类重写的虚函数地址、派生类自己的虚函数地址三部分。
  6. 虚函数表本质是一个函数指针数组,一般情况这个数组在后面放一个0x00000000作为结束标志。(不过这个C++规定这个,取决于各个编译器)

5.有关虚函数和虚函数表的存储位置

虚函数和普通函数一样,编译好后是一段指令,存在代码段(常量区),只是虚函数的地址又存在虚函数表中。

虚函数表的存储位置C++标准中没有规定,取决于不同编译器。在VS中,虚函数表是存储在常量区的。

下面这段代码可以验证下在VS中虚函数表是存储在常量区的。

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

        

五.常见问题

1. 什么是多态?
答:多态分为静态多态:函数重载;和动态多态:继承中的虚函数重写+基类指针引用。


2. 什么是重载、重写 ( 覆盖 ) 、重定义 ( 隐藏 ) ?
答:参考上述内容。


3. 多态的实现原理?
答:静态多态:函数名修饰规则;动态多态:虚函数表。


4. inline 函数可以是虚函数吗?
答:可以,不过编译器就忽略 inline 属性,这个函数就不再是inline,因为虚函数要放到虚表中去。


5. 静态成员可以是虚函数吗?
答:不能。静态成员函数属于类本身,而不是类的某个特定对象。它可以通过类名直接调用,不需要类的实例。虚函数是为了实现多态性,允许在运行时根据对象的实际类型调用相应的函数。虚函数需要依赖于对象的动态类型,而静态成员函数不依赖于任何对象的类型。两者的这种特性有所冲突,所以禁止将静态成员函数声明为虚函数。

6. 构造函数可以是虚函数吗?
答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。


7. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?
答:可以,并且最好把基类的析构函数定义成虚函数。参考上述内容。


8. 对象访问普通函数快还是虚函数更快?
答:首先如果是普通对象,是一样快的。如果是指针对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。

9. 虚函数表是在什么阶段生成的,存在哪的?
答:虚函数表是在编译阶段就生成的,一般情况下存在代码段( 常量区 ) 的。

10. C++ 菱形继承的问题?虚继承的原理?
答:参考继承。注意这里不要把虚函数表和虚基表搞混了。

11. 什么是抽象类?抽象类的作用?
答:参考上述内容。抽象类强制重写了虚函数,另外抽象类体现出了接口继承关系。


拜拜,下期再见😏

摸鱼ing😴✨🎞

 

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

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

相关文章

13 Midjourney从零到商用·实战篇:漫画设计一条龙

大家好&#xff0c;经过前面十三篇文章,相信大家已经对Midjourney的使用非常熟悉了&#xff0c;那么现在我们开始进行实际的项目操作啦&#xff0c;想想是不是有点小激动呀&#xff0c;本篇文章为大家带来Midjourney在漫画制作领域的使用流程&#xff0c;同样也适用于现在短视频…

[C语言]第十节 函数栈帧的创建和销毁一基础知识到高级技巧的全景探索

10.1. 什么是函数栈帧 我们在写 C 语言代码的时候&#xff0c;经常会把一个独立的功能抽象为函数&#xff0c;所以 C 程序是以函数为基本单位的。 那函数是如何调用的&#xff1f;函数的返回值又是如何待会的&#xff1f;函数参数是如何传递的&#xff1f;这些问题都和函数栈帧…

高德地图2.0 绘制、编辑多边形覆盖物(电子围栏)

1. 安装 npm i amap/amap-jsapi-loader --save移步&#xff1a;官方文档 2. map组件封装 <script lang"ts" setup> import AMapLoader from amap/amap-jsapi-loader import { onMounted, ref } from vue import { propTypes } from /utils/propTypesdefineO…

开发小程序

由于之前购入的阿里云ECS放着落灰&#xff0c;碰巧又看到个有趣的项目&#xff0c;于是就做了个生成头像的小程序…由于第一次完整发布小程序&#xff0c;记录一下遇到的问题 小程序名称&#xff1a;靓仔创意头像 &#x1f602; 关于小程序 接口请求&#xff0c;在开发过程中…

在 Mac 上安装双系统会影响性能吗,安装双系统会清除数据吗?

在 Mac 系统安装并使用双系统已经成为了许多用户办公的选择之一&#xff0c;双系统可以让用户在 Mac 上同时运行 Windows 或其他操作系统。然而&#xff0c;许多用户担心这样做会对 Mac 的性能产生影响。 接下来将给大家介绍 Mac 装双系统会影响性能吗&#xff0c;Mac装双系统…

C++速通LeetCode简单第20题-多数元素

方法一&#xff1a;暴力解法&#xff0c;放multiset中排序&#xff0c;然后依次count统计&#xff0c;不满足条件的值erase清除。 class Solution { public:int majorityElement(vector<int>& nums) {int ans 0;multiset<int> s;for(int i 0;i < nums.s…

孙怡带你深度学习(2)--PyTorch框架认识

文章目录 PyTorch框架认识1. Tensor张量定义与特性创建方式 2. 下载数据集下载测试展现下载内容 3. 创建DataLoader&#xff08;数据加载器&#xff09;4. 选择处理器5. 神经网络模型构建模型 6. 训练数据训练集数据测试集数据 7. 提高模型学习率 总结 PyTorch框架认识 PyTorc…

UE5安卓项目打包安装

Android studio安装 参考&#xff1a;https://docs.unrealengine.com/5.2/zh-CN/how-to-set-up-android-sdk-and-ndk-for-your-unreal-engine-development-environment/ 打开android studio的官网&#xff1a;Download Android Studio & App Tools - Android Developers …

深度学习-生成式检索-论文速读-2024-09-14

深度学习-生成式检索-论文速读-2024-09-14 前言: 生成式检索&#xff08;Generative Retrieval&#xff0c; GR&#xff09;是一种结合了生成模型和检索系统的人工智能技术方法。这种方法在处理信息检索任务时&#xff0c;不仅依赖于已有数据的检索&#xff0c;还能生成新的、…

解锁SQL无限可能 | 基于SQL实现的一种时序数据的波峰个数检测算法

目录 0 算法原理 1 数据准备 2 问题分析 3 小结 数字化建设通关指南专栏原价99&#xff0c;现在活动价39.9&#xff0c;按照阶梯式增长&#xff0c;直到恢复原价 0 算法原理 波峰识别算法 序列数据是按照时间进行采集&#xff0c;其中400个点一个周期&#xff0c;一条数据…

【零散技术】Odoo17通过Controller下载PDF

序言:时间是我们最宝贵的财富,珍惜手上的每个时分 Odoo作为一款开源ERP&#xff0c;拥有极佳的拓展性&#xff0c;Odoo的Controller框架也让它具备了作为微信小程序后端的能力&#xff0c;那么就存在 需要通过小程序来下载PDF的业务情况。 目录 1.功能代码 1.1 manifest 设置 …

Tensorflow—第五讲卷积神经网络

本讲概述 卷积实际上就是特征提取。本讲我们先了解学习卷积神经网络基础知识&#xff0c;再一步步地学习搭建卷积神经网络&#xff0c;最后会运用卷积神经网络对cifar10 数据集分类。在本讲的最后附上几个经典卷积神经网络&#xff1a;LeNet、AlexNet、VGGNet、InceptionNet和…

在Linux中安装FFmpeg

在Linux中安装FFmpeg有两种方法。 安装FFmpeg&#xff08;方法一&#xff09; 第一步&#xff0c;下载FFmpeg。 登录地址&#xff1a;John Van Sickle - FFmpeg Static Builds下载安装包ffmpeg-git-amd64-static.tar.xz。然后使用WinSCP将安装包上传到文件夹/usr/local/src中…

vue2基础系列教程之插槽slot你不得不知道的知识点及面试高频问题

vue2中对插槽的介绍&#xff0c;花了大量的章节篇幅&#xff0c;可想而知&#xff0c;它在框架中的重要性。 slot及slot-scope自 2.6.0 起被废弃。新推荐的语法请查阅v-slot,就语法我们这里就一笔带过&#xff0c;主要学习新的语法 你不能不知道的slot知识点 插槽的作用&#…

C++知识要点总结笔记

文章目录 前言一、c基础1.指针和引用指针和引用的区别函数指针 2.数据类型整型 short int long 和 long long无符号类型强制类型转换怎样判断两个浮点数是否相等&#xff1f; 3.关键字conststaticconst和static的区别define 和 typedef 的区别define 和 inline 的区别define和c…

PostgreSQL(PG)(二十二)

&#x1f33b;&#x1f33b; 目录 &#x1f33b;&#x1f33b; 一、PostgreSQL 简介1.1、PG 的历史1.2、PG的社区1.2.1 纯社区1.2.2 完善的组织结构1.2.3 开源许可独特性 1.3 、PostgreSQL与MySQL的比较 二、PostgresQL的下载安装2.1、Windows上安装 PostgreSQL2.2、远程 连接 …

Pikachu靶场之XSS

先来点鸡汤&#xff0c;少就是多&#xff0c;慢就是快。 环境搭建 攻击机kali 192.168.146.140 靶机win7 192.168.146.161 下载zip&#xff0c;pikachu - GitCode 把下载好的pikachu-master&#xff0c;拖进win7&#xff0c;用phpstudy打开网站根目录&#xff0c;.....再用…

Unity3D下如何播放RTSP流?

技术背景 在Unity3D中直接播放RTSP&#xff08;Real Time Streaming Protocol&#xff09;流并不直接支持&#xff0c;因为Unity的内置多媒体组件&#xff08;如AudioSource和VideoPlayer&#xff09;主要设计用于处理本地文件或HTTP流&#xff0c;而不直接支持RTSP。所以&…

【话题讨论】AI时代程序员核心力:技术深耕,跨界学习,软硬兼备

目录 引言 一、AI辅助编程对程序员工作的影响 1.1 AI工具如何提升工作效率 1.2 AI工具的风险 1.3 应对策略 二、程序员应重点发展的核心能力 2.1 核心竞争力 2.2 企业和教育机构的调整 三、人机协作模式下的职业发展规划 3.1 持续学习的重要性 3.2 选择适合自己的…

Python3网络爬虫开发实战(17)爬虫的管理和部署(第一版)

文章目录 一、 Scrapyd 分布式部署1.1 了解 Scrapyd1.2 准备工作1.3 访问 Scrapyd1.4 Scrapyd 的功能1.5 ScrapydAPI 的使用 二、Scrapyd-Client 的使用2.1 准备工作2.2 Scrapyd-Client 的功能2.3 Scrapyd-Client 部署 三、Scrapyd 对接 Docker3.1 准备工作3.2 对接 Docker 四、…