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

秒懂C++之多态

fe594ea5bf754ddbb223a54d8fb1e7bc.gif

目录

一. 多态的概念

二. 多态的定义及实现

多态的构成条件

虚函数重写的例外

协变(基类与派生类虚函数返回值类型不同)

析构函数的重写(基类与派生类析构函数的名字不同)

练习例题

final

override

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

三. 抽象类

四. 多态的原理

虚函数表

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


一. 多态的概念

多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态

就比如拿我们的支付宝红包举例子,老用户往往会扫出很小的红包,而新用户却往往能扫到大额红包~

二. 多态的定义及实现

多态的构成条件

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了 Person。Person对象买票全价,Student对象买票半价。
那么在继承中要构成多态还有两个条件
  • 必须通过父类的指针或者引用调用虚函数
  • 被调用的函数必须是虚函数(加上virtual),且派生类必须对基类的虚函数进行重写(函数名相同,参数相同,返回值相同)
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 ps;Student st;Func(ps);Func(st);return 0;
}

正常情况下调用函数是通过其指针或引用类型然后去调该类型的类成员函数~而在多态中是看父类指针或引用的对象,通过对象去调用该对象的类成员函数~

虚函数重写的例外

协变(基类与派生类虚函数返回值类型不同)

class A {};
class B : public A {};
class Person {
public:virtual A* f() { return new A; }
};
class Student : public Person {
public:virtual B* f() { return new B; }
};

重写是必须要求要相同返回值的,但也可以有例外,就是返回值可以不同,但是两个返回值必须得是指针或引用,同时得保证这两个返回值构成继承关系。即父类虚函数返回父类对象的指针或者引用,子类虚函数返回子类对象的指针或者引用时称为协变~

ps:不常出现,了解即可~

析构函数的重写(基类与派生类析构函数的名字不同)

class Person {
public:~Person() { cout << "~Person()" << endl;}
};// 重写实现
class Student : public Person {
public:~Student(){ // delete _ptr;cout << "~Student()" << endl;}
};int main()
{Person* p1 = new Person;delete p1;Person* p2 = new Student;delete p2;return 0;
}

如果我们没有用虚函数,那么在代码执行中p1去调用父类的析构函数,p2也会去调用父类的析构函数~

可是这样忽略了一个问题:如果我们new出来的子类当中有额外的资源呢?本来子类的析构函数可以去清理这个额外资源,但由于正常调用而去调用父类的析构导致内存泄漏~

所以为了能让析构函数也形成多态的效果,我们选择使用虚函数进行重写~

而且在同名函数这块也进行了特殊处理,一律把析构的函数名当作destructor来看~使重写的条件名字,最后在父类指针的调用下根据指向对象来调用对象的成员函数~

练习例题

ps小知识点:
  • 如果父类的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~ 接下来我来为大家分析解答~
首先我们发现子类func没有virtual而父类有,那么这也是能构成虚函数重写的~
然后我们可以得出在父类中的隐藏this指针类型是父类的,而父类指针去调用func函数是满足多态条件的,那么就要去找到所调用的对象而非类型,而test函数是被指向B对象的指针所调用的~那么说明多态作用下func调用的是B类的成员函数func~
又因为B类中的func函数中virtual是没有的,那么它只能去用父类的函数声明然后再结合自己的定义作组合,所以在这个过程中子类的func函数是用了父类func中的val再加上实现的B->最终形成答案B~
如果是普通对象调用或者非父类指针去调用那也就没那么多事了~

final

final:修饰虚函数,表示该虚函数不能再被重写

如果我们想要让子类无法继承父类有两种方法:

法一:让父类构造私有

class A 
{
public:
protected:int _a;
private:A(){}
};class B : public A
{};int main()
{B bb;return 0;
}

这样子类就无法实例化出对象了,因为子类构造必须调用父类构造~

法二:使用final

class A final
{
public:
protected:int _a;
private:A(){}
};class B : public A
{};int main()
{B bb;return 0;
}

override

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

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

三. 抽象类

在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口
类),抽象类不能实例化出对象
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 c;//无法实例化出对象Car* pBenz = new Benz;pBenz->Drive();Car* pBMW = new BMW;pBMW->Drive();return 0;
}

子类只有进行虚函数重写避开去调用父类的纯虚函数就可以实例化了~从另一种角度上看抽象类也有强迫子类进行多态调用的提醒作用~

四. 多态的原理

虚函数表

//sizeof(Base)是多少?
class Base
{
public:virtual void Func1(){cout << "Func1()" << endl;}
private:int _b = 1;char _ch;
};int main()
{cout << sizeof(Base) << endl;Base  bb;return 0;
}

很多人会说是8,但实际上却为12。因为在有了虚函数后对象里面还会多出一个指针~

该指针内放置虚函数Func1的地址~
我们再来修改一下代码~
class Base
{
public:void Func1(){cout << "Base::Func1()" << endl;}virtual void Func2(){cout << "Base::Func2()" << endl;}void Func3(){cout << "Base::Func3()" << endl;}
private:int _b = 1;char _ch = 'a';
};class Derive : public Base
{
public:virtual void Func1(){cout << "Derive::Func1()" << endl;}
private:int _d = 2;
};int main()
{cout << sizeof(Base) << endl;Base  bb;cout << sizeof(Derive) << endl;Derive dd;return 0;
}

我们在父类多添加了一个虚函数和普通函数,在子类添加了一个虚函数

不出我们意外,都多出了一个指针~
其实虚函数建立的时候在内存中会有虚函数表这种对象~而虚函数表也就是函数指针的数组~
当有多个虚函数的时候就会出现一个指针(_vfptr)去指向虚函数数组(虚函数表),然后访问数组里面各个虚函数的地址~
子类中是没有func2函数的,那么就会去拿父类中func2的地址,而子类是有自己的func1函数的,那么在虚函数表中就会让子类的func1地址去覆盖父类的func1地址~
ps:虚函数表中先声明的放在前面~

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

我们再来对虚函数表做一些补充~
  • 首先如果是普通调用,那么函数地址是在编译时就确定的。在编译链接的时候从符号表中找到函数地址,然后去调用它。
  • 而在多态调用中是通过指针进入对应对象虚函数表,在里面找到虚函数地址并成功调用它~

 

下面我们来了解一些多继承关系中的虚函数表~
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(*VF_PTR)();
void PrintVFT(VF_PTR* vft)
{for (size_t i = 0; vft[i] != nullptr; i++){printf("[%d]:%p->", i, vft[i]);VF_PTR f = vft[i];f();//(*f)();}cout << endl << endl;
}int main()
{Derive d;cout << sizeof(d) << endl;Base1* ptr1 = &d;Base2* ptr2 = &d;PrintVFT((VF_PTR*)(*(int*)ptr1));PrintVFT((VF_PTR*)(*(int*)ptr2));return 0;
}

由于vs的监视窗口无法查看2个以上的虚函数,所以我们这里选择人工打印出所属对象的虚函数表里面的内容~

  • 首先我们需要取得类对象中的前4个字节,因为那里代表指向虚函数表的指针~所以我们对切片过的指针ptr1进行强转为int*类型再解引用就可以拿到其指针地址了~
  • 然后我们再通过对指针强转为(VF_PTR*类型)因为我们的打印函数中打印的是函数指针,所以需要实参与形参类型相同以便接收~
  • 最后我们再把得到的函数指针去回调func函数形成多态的效果~

在多继承中有两张虚函数表,一张代表Base1,一张代表Base2~
而在Base1表中原有的虚函数会被子类独有的虚函数覆盖,例如子类只复写了func1与func3虚函数,那么在Base1中就会被子类的func1与func3虚函数覆盖~
而在Base2表中同样如此,不过需要注意的是func3最后只放在了先声明的那个父类中,Base2是不放func3的~
总结:对于func3这种在父类没有的虚函数,一般会放在最先声明的类中~至于其他的该拷贝拷贝,该替换替换~
最后是其他知识点的总结:
  1. inline函数可以是虚函数吗?答:可以,不过编译器就忽略inline属性,这个函数就不再是 inline,因为虚函数要放到虚表中去。
  2. 静态成员可以是虚函数吗?答:不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
  3. 构造函数可以是虚函数吗?答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。
  4. 对象访问普通函数快还是虚函数更快?答:首先如果是普通对象,是一样快的。如果是指针 对象或者是引用对象,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函 数表中去查找。
  5. 虚函数表是在什么阶段生成的,存在哪的?答:虚函数表是在编译阶段就生成的,一般情况 下存在代码段(常量区)的。          

相关文章:

秒懂C++之多态

目录 一. 多态的概念 二. 多态的定义及实现 多态的构成条件 虚函数重写的例外 协变(基类与派生类虚函数返回值类型不同) 析构函数的重写(基类与派生类析构函数的名字不同) 练习例题 final override 重载、覆盖(重写)、隐藏(重定义)的对比 三. 抽象类 四. 多态的原理…...

C语言:求最大数不用数组

&#xff08;1&#xff09;题目&#xff1a; 输入一批正数用空格隔开&#xff0c;个数不限&#xff0c;输入0时结束循环&#xff0c;并且输出这批整数的最大值。 &#xff08;2&#xff09;代码&#xff1a; #include "stdio.h" int main() {int max 0; // 假设输入…...

零门槛成为HelpLook推荐官,邀好友加入,奖励享不停!

什么&#xff01;&#xff1f; 还有谁不知道HelpLook推荐官计划&#xff01; 只需要简单地注册加入 在好友成功订阅套餐之后 可一次性获得20%的丰厚现金返佣 HelpLook是一款快速搭建AI知识库的系统&#xff0c;并帮助企业0代码搭建帮助中心、FAQs、SOPs、产品文档、说明书和…...

基于python的图书馆大数据可视化分析系统设计与实现

博主介绍&#xff1a; 大家好&#xff0c;本人精通Java、Python、C#、C、C编程语言&#xff0c;同时也熟练掌握微信小程序、Php和Android等技术&#xff0c;能够为大家提供全方位的技术支持和交流。 我有丰富的成品Java、Python、C#毕设项目经验&#xff0c;能够为学生提供各类…...

利用formdata自动序列化和xhr上传表单到后端

//FormData对象是XMLHTTPRequest level2新增的类型&#xff0c;它可以自动序列化表单内容&#xff0c;不再需要我们去写序列化表单方法&#xff1b; FormData()即可以直接把整个表单给它&#xff0c;也可以分别使用append(‘字段’,‘值’)方法给FormData()&#xff1b; 现在就…...

视频号小店大地震?还好我看了原文

关注卢松松&#xff0c;会经常给你分享一些我的经验和观点。 我X&#xff0c;如果不是看了原文&#xff0c;我差点也上当了。虽然视频号小店关闭了450个类目&#xff0c;但又重新开放了412个类目啊。 昨天&#xff08;8月9日&#xff09;&#xff0c;视频号一口气发了10个公…...

Genymotion adb shell

Genymotion 账户是 qq邮箱 参考 Ubuntu 20.04 安装 Android 模拟器 Genymotion https://www.zzzmh.cn/post/553cd96d4e47490a90b3302a76a93c0d Genymotion adb shell adb shell C:\Program Files\Genymobile\Genymotion\tools>adb shell lsusb Bus 001 Device 001: ID …...

探索AI与社交的交汇点:看Facebook如何引领智能化革命

在当今数字化时代&#xff0c;人工智能&#xff08;AI&#xff09;正成为各大科技公司变革的重要驱动力。作为全球领先的社交媒体平台&#xff0c;Facebook&#xff08;现Meta Platforms&#xff09;正处于这一智能化革命的前沿。通过不断创新和应用AI技术&#xff0c;Facebook…...

JVM 加载阶段 Class对象加载位置是在 堆中还是方法区?

在JVM&#xff08;Java虚拟机&#xff09;的类加载过程中&#xff0c;Class对象的加载位置涉及到堆&#xff08;Heap&#xff09;和方法区&#xff08;Method Area&#xff09;两个关键区域。具体来说&#xff0c;类的加载阶段涉及到将类的.class文件中的二进制数据读入到内存中…...

Android 获取短信验证

Android 获取短信验证 Android 获取短信验证 输入发短信的手机号&#xff0c;点击获取验证码&#xff0c;等接收到验证码后就会自动获取 SmsReceiver.Java import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; impor…...

负载均衡详细概念介绍之(四层和七层实现)

目录 一、负载均衡介绍 1.1什么是负载均衡 ​编辑 1.2 为什么要用负载均衡 二、负载均衡的类型 2.1 通过一些硬件实现 2.2 四层负载均衡 2.3 七层负载均衡 三、四层和七层的区别 及特点 一、负载均衡介绍 1.1什么是负载均衡 负载均衡:Load Balance&#xff0c;简称LB&a…...

力扣 | 递增子序列 | 动态规划 | 最长递增子序列、最长递增子序列的个数、及其变式

文章目录 一、300. 最长递增子序列二、673. 最长递增子序列的个数三、变式1、646. 最长数对链2、1218. 最长定差子序列3、1027. 最长等差数列4、354. 俄罗斯套娃信封问题5、1964. 找出到每个位置为止最长的有效障碍赛跑路线 最长递增子序列&#xff1a;原序-递增数值问题 最长定…...

008 | 基于RNN和LSTM的贵州茅台股票开盘价预测

基于RNN和LSTM的贵州茅台股票开盘价预测 项目简介&#xff1a; 本项目旨在通过使用Tushare下载贵州茅台的股票数据&#xff0c;并基于这些历史数据&#xff0c;使用TensorFlow 2.0实现循环神经网络&#xff08;RNN&#xff09;和长短期记忆网络&#xff08;LSTM&#xff09;来…...

尚硅谷谷粒商城项目笔记——六、使用navciat连接docker中的mysql容器【电脑CPU:AMD】

六、使用navciat连接docker中的mysql容器 注意&#xff1a; 因为电脑是AMD芯片&#xff0c;自己知识储备不够&#xff0c;无法保证和课程中用到的环境一样&#xff0c;所以环境都是自己根据适应硬件软件环境重新配置的&#xff0c;这里的虚拟机使用的是VMware。 1navicat免费…...

【git】本地更改了,但又想直接从远端拉取覆盖本地(放弃本地更改前行下载)

拉取时错误信息 error: cannot pull with rebase: You have unstaged changes. error: Please commit or stash them.个错误信息表明你在尝试使用git pull --rebase命令时&#xff0c;你的工作目录中存在未提交的更改&#xff08;即未暂存&#xff08;unstaged&#xff09;的更…...

基于JSP的书店仓库管理系统

你好呀&#xff0c;我是计算机学姐码农小野&#xff01;如果有相关需求&#xff0c;可以私信联系我。 开发语言&#xff1a;JSP 数据库&#xff1a;MySQL 技术&#xff1a;JSPJava 工具&#xff1a;ECLIPSE、Tomcat 系统展示 首页 管理员功能模块 用户功能模块 员工功能模…...

pytorch框架保存和加载模型

在 PyTorch 中&#xff0c;有几种常见的方法来保存和加载模型&#xff0c;具体方法取决于你想保存什么内容&#xff08;例如&#xff0c;只保存模型的权重&#xff0c;还是保存整个模型&#xff09;。下面我将介绍几种常见的保存和加载模型的方法。 1、保存和加载模型的 state…...

开发输出防护栏以检测GPT-4o幻觉

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…...

代码复现,代码改进,算法复现,模型复现

目前空闲可接硕士&#xff0c;博士&#xff0c;代码复现&#xff0c;改进代码&#xff0c;文献复现&#xff0c;算法复现&#xff0c;模型复现&#xff0c;文章复现&#xff0c;科研复现&#xff0c;可定制创新点&#xff0c;对比&#xff0c;模块&#xff0c;创新思路&#xf…...

基于STM32开发的智能机器人系统

目录 引言环境准备工作 硬件准备软件安装与配置系统设计 系统架构硬件连接代码实现 初始化代码控制代码应用场景 自动导航机器人家用服务机器人常见问题及解决方案 常见问题解决方案结论 1. 引言 智能机器人通过整合传感器、控制器和执行机构&#xff0c;能够自主完成环境感…...

【数据结构题目】循环队列,以及队列实现栈的模拟

前言&#xff1a; &#x1f31f;&#x1f31f;Hello家人们&#xff0c;这期讲解数据结构队列的基础知识&#xff0c;希望你能帮到屏幕前的你。 &#x1f4da;️上期博客在这里&#xff1a;http://t.csdnimg.cn/oOkvk &#x1f4da;️感兴趣的小伙伴看一看小编主页&#xff1a;G…...

大数据CloudSim应用实践:基于CloudSimExamle6.java修改(超详细教程)

文章目录 大数据CloudSim应用实践&#xff1a;基于CloudSimExamle6.java修改&#xff08;超详细教程&#xff09;1 准备1.1 操作系统1.2 软件 2 安装JDK2.1 安装JDK 3 配置Eclipse集成开发环境3.1 启动Eclipse3.2 配置Java运行时环境JRE 4 创建Java项目4.1 创建项目4.2 导入jar…...

完美解决浏览器的输入框自动填入时,黄色背景问题,以及图标被遮住问题(最新)

用图说话↓↓↓ 首先用代码解决黄色背景问题&#xff0c;box-shadow颜色设置透明即可&#xff0c;延时渲染时间可修改为更久 :deep(input:-webkit-autofill) {box-shadow: 0 0 0 1000px transparent !important;/* 浏览器记住密码的底色的颜色 */-webkit-text-fill-color: #f…...

C 语言中的头文件

1、C 语言中 include <> 与include “” 的区别? #include < > 引用的是编译器的类库路径里面的头文件。 #include " " 引用的是你程序目录的相对路径中的头文件&#xff0c;如果在程序目录没有找到引用的头文件则到编译器的类库路径的目录下找该头文…...

数据结构复杂度

文章目录 一. 数据结构前言1.1 数据结构1.2 算法 二. 算法效率2.1 时间复杂度2.1.1 T(N)函数式2.1.2 大O的渐进表示法 2.2 空间复杂度2.3 常见复杂度比较 2.3 复杂度算法题1.2. 一. 数据结构前言 1.1 数据结构 什么是数据结构呢&#xff1f;打开一个人的主页&#xff0c;有很…...

MySQL基础篇

一、MySQL概述 MySQL是一个数据库管理系统&#xff0c;由瑞典MySQL AB公司开发&#xff0c;属于Oracle推出的产品。MySQL是最流行的关系型数据库管理系统之一&#xff0c;在WEB应用方面&#xff0c;MySQL是最好的RDBMS&#xff08;关系数据库管理系统&#xff09; &#xff0c…...

详解C++中的四种强制转换reinterpret_cast / const_cast / static_cast / dynamic_cast

目录 1.reinterpret_cast 2.const_cast 3.static_cast 4.dynamic_cast 例子 C中存在四种强制转换&#xff1a;reinterpret_cast / const_cast / static_cast / dynamic_cast 1.reinterpret_cast 格式 &#xff1a; reinterpret_cast<type_id> (expression) 用于类型…...

Word中加载Mathtype后粘贴复制快捷键(Ctrl+C/V)不能使用

操作环境 windows 11操作系统 word版本2021 mathtype版本7.4 这个问题只出现在word中&#xff0c;在excel和ppt中都不存在这个问题&#xff0c;而且之前在另一台电脑中使用word2016版本并没有这种问题的&#xff0c;然后网上搜了一下有不少人有这种问题&#xff0c;word直接取…...

Linux硬件-bios

作者介绍&#xff1a;简历上没有一个精通的运维工程师。希望大家多多关注作者&#xff0c;下面的思维导图也是预计更新的内容和当前进度(不定时更新)。 在Linux的服务器领域&#xff0c;我们能接触的到硬件其实挺多的&#xff0c;但是在这些硬件我们根据我们的需要去使用的时候…...

VisionPro二次开发学习笔记12-使用CogToolGroup控件进行图像检测

本示例演示了如何通过图像数据库使用 CogImageFileTool&#xff0c;并将其放入 CogToolGroup 中&#xff0c;对于数据库中的每个图像运行一次检测. 当用户按下 RunTest 按钮时&#xff0c;程序执行以下操作&#xff1a; 如果工具组中没有 CogImageFileTools&#xff0c;它将显…...