【C++】多态:深度剖析(多态、虚函数、抽象类、底层原理)

温馨提示:在观看本文前确保已经了解了C++中继承的相关知识,若不了解,可以查看我的这篇文章进行学习:【C++】继承:深度剖析-CSDN博客icon-default.png?t=O83Ahttps://blog.csdn.net/2301_80555259/article/details/141829528?spm=1001.2014.3001.5501


目录

一.多态的概念

二.多态的定义和实现

三.虚函数

1.概念

2.重写

3.重写的两个例外

3.1 协变(返回类型不同)

3.2 析构函数的重写(函数名不同)

4.C++11:override和final

5.重载、重写、隐藏的对比

四.抽象类

五.虚函数原理

六.多态的原理


一.多态的概念

通俗来讲,多态就是拥有多种状态,父子对象完成相同的任务却会产生不同的结果;例如普通人和学生去买票(进行相同的任务),普通人(父类)是全价,而学生(子类)是半价,这就是多态的一种现实体现。

从程序角度来讲,多态是指同一个函数名可以根据调用对象的不同而具有不同的实现。多态分为两种类型:编译时多态(静态多态)和运行时多态(动态多态) 

  • 编译时多态:通过函数重载运算符重载实现,是在编译阶段确定函数调用
  • 运行时多态:通过虚函数和继承实现,是在运行阶段确定函数调用

二.多态的定义和实现

多态的实现通常依赖于虚函数。在基类(父类)中声明虚函数,然后在派生类(子类)中进行重写(覆盖)。通过基类指针或引用调用虚函数时,将根据对象的实际类型调用相应的派生类函数

在继承中构成多态需要有两个前提条件:

  1. 必须通过基类的指针或者引用调用函数
  2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

那么,什么是虚函数?接下来就对虚函数进行深度解析


三.虚函数

1.概念

虚函数是在基类中使用关键字virtual进行声明的成员函数,它的存在允许在派生类中进行该函数的重写(覆盖)。

class Person
{
public://虚函数virtual void BuyTicket() { cout << "买票-全价"; }
};

2.重写

虚函数的重写(Override)是指在派生类中重新实现上述基类中已经声明为虚函数的函数。在进行重写时,子类中的虚函数的返回类型、函数名、参数列表类型必须与基类中虚函数完全一致(参数名称、缺省值可以不相同)

注意:在重写基类虚函数时,派生类的虚函数可以不加virtual关键字,这依然构成重写,因为继承后基类的虚函数在派生类中依然保持虚函数属性,但这种写法其实不规范,不建议这样书写

3.重写的两个例外

3.1 协变(返回类型不同)

派生类重写虚函数其返回类型为派生类指针或引用,而基类虚函数其返回类型为基类指针或引用时,此时尽管两虚函数返回类型不同,也依然是虚函数重写,称为协变

class A
{
public://返回类型为基类指针virtual A* f() { return new A; }
};class B:public A
{
public://返回类型为派生类指针virtual B* f(){return new B;}
};

3.2 析构函数的重写(函数名不同)

如果基类的析构函数为虚函数,那么派生类的析构函数只要定义,无论是否加virtual关键字都构成重写,尽管函数名字不同。这里其实是编译器做了特殊处理,编译后析构函数的名称统一处理为destructor

至于为什么要这样处理,可以用以下样例解释:

class A
{
public:virtual ~A(){cout << "~A()" << endl;}
};class B :public A
{
public://不加virtual也依旧构成重写~B(){cout << "~B()" << endl;delete _p;}
protected:int* _p = new int[10];
};int main()
{A* p1 = new A;A* p2 = new B;delete p1;delete p2;
}

若基类A和派生类B不构成虚函数重写的话,那么此处调用delete p2时,p2是基类A的指针,只会调用A的析构,从而没有调用B的析构,此时B内申请的资源int* _p就没有释放,造成了内存泄漏,如下图,为了防止这种情况,才使用继承中的虚函数析构

4.C++11:override和final

1.override

派生类虚函数的格式有严格要求,返回类型、函数名、参数类型都必须完全相同,但这在编译时是不会报错的,哪怕函数名打错了一个字母也不会报错让你发现,这时就需要override来检查是否正确地重写了该函数了,例如:

class Car 
{
public:virtual void Drive() {}
};class Benz :public Car 
{
public:virtual void Drive() override {}
};

此时如果函数名写错就会在编译阶段报错了

2.final

final用于在派生类中阻止对虚函数的进一步重写,或者在类定义中阻止类被继续派生

//阻止该类继续派生,该类就是"最终"(final)类
class Base final {// ...
};
//阻止该虚函数被继续重写
class Base {
public:virtual void f() final {// ...}
};

5.重载、重写、隐藏的对比


四.抽象类

在虚函数的后面加上 = 0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(接口类),抽象类不能实例化出对象,只有其派生类重写纯虚函数后,派生类才能实例化出对象。也就是说,纯虚函数规定了派生类必须重写。

 实际上很好理解,例如把动物当做一个抽象类,你能实例化找出一个“动物”吗?动物是个抽象的概念,无法实际存在,只有将其重写为猫、狗等实际存在的动物后,才能实例化地找到。


五.虚函数原理

在调试一个含有虚函数的类Base时,我们发现该类除了成员变量_base外,还包含一个变量_vfptr,这个变量就是虚函数表指针(virtual function pointer)

 一个含有虚函数的类中至少都有一个虚函数表指针,该指针指向的是一个数组,该数组是储存函数指针的。也就是说,该指针指向一个函数指针数组。该数组存放虚函数的地址,不同的派生类该表也不同。

 通过一下例子,我们可以再详细谈谈这整个过程

class Base
{
public:virtual void Func1(){cout << "Func1()" << endl;}virtual void Func2(){cout << "Func2()" << endl;}
private:int _base = 1;
};class Derive :public Base
{
public:virtual void Func1(){cout << "Func111()" << endl;}//virtual void Func2()//{//    cout << "Func222()" << endl;//}
protected:int _derive = 2;
};int main()
{Base b;Derive d;return 0;
}

通过该结果可以发现,派生类Derive的虚函数表与基类Base是不同的,Derive中对虚函数Func1进行了重写,但没有对Func2重写,这就导致了d类的虚表中第一个Func1的地址被覆盖了,而第二个Func2没有被覆盖,和基类相同。因此继承时,派生类是继承了基类的虚函数表的,不过当虚函数重写时就进行函数地址的覆盖而已。

顺带一提,如果在Derive中在新写一个虚函数,vs的调试窗口上d类的虚表中是看不见的,但可以通过内存窗口去观察


六.多态的原理

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 man;Func(man);Student Jack;Func(Jack);return 0;
}

根据下图的红色箭头可以发现,p是指向man对象时,p.BuyTicket在虚表中找到的是Person::BuyTicket,而蓝色箭头,p是指向Jack对象时,p.BuyTicket在虚表中找到的就是Student::BuyTicket,这不是在编译时确定的,而是运行以后到对象中找到的。

当然这里的核心依然是切割,编译器看到的是父类,不过是指向子类时是切割过去的而已,里面的虚表也是被子类覆盖后的结果。

好了,多态的知识解析就是这些,最后再来一道很容易错的习题来结束吧

以下程序输出结果是什么()
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;
}

答对了吗?

解析:

 

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

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

可视化大屏看阿里,阿里出品,必属精品。

阿里云有自己的可视化平台——dataV&#xff0c;经常会出一些高颜值、强交互的大屏&#xff0c;本期为大家分享一波。

c++难点核心笔记(一)

文章目录 前言C的应用领域 核心编程内存分区模型1.程序运行前2.程序运行后3.new操作符引用 函数1.概述和函数原型2.函数的定义和参数3.使用函数处理不同类型的数据4.微处理器如何处理函数调用函数的分文件编写 指针和引用什么是指针动态内存分配使用指针时常犯的编程错误指针编…

新一代图像生成E2E FT:深度图微调突破

文章地址&#xff1a;Fine-Tuning Image-Conditional Diffusion Models is Easier than You Think 项目主页&#xff1a;https://gonzalomartingarcia.github.io/diffusion-e2e-ft/ 代码地址&#xff1a;https://github.com/VisualComputingInstitute/diffusion-e2e-ft 机构&am…

数据结构:搜索二叉树

前言 在前面我们已经学习了二叉树的基础操作&#xff0c;但是&#xff0c;仅仅是二叉树&#xff0c;没有太大的作用啊&#xff0c;存数据效果没有顺序表和链表好&#xff0c;那为啥还要学二叉树呢&#xff1f; 这不就来了嘛&#xff0c;给二叉树增加一些性质&#xff0c;作用不…

剑侠情缘c++源码全套(增加缺失的头文件和相关的库,其它网上流传的都是不全的)剑网三源码

剑侠情缘c源码全套&#xff08;增加缺失的头文件和相关的库&#xff0c;其它网上流传的都是不全的&#xff09; 下载地址&#xff1a; 通过网盘分享的文件&#xff1a;剑侠情缘c源码全套&#xff08;增加缺失的头文件和相关的库&#xff0c;其它网上流传的都是不全的&#xff0…

飞睿智能3公里WiFi实时图传模块,隧道高速无线传输抗干扰,实时不卡顿

在数字化快速发展的今天&#xff0c;无线通信技术日新月异&#xff0c;其中WiFi实时图传模块凭借其高效、稳定、便捷的传输特性&#xff0c;正逐渐在各个领域崭露头角。特别是当我们谈论到3公里WiFi实时图传模块时&#xff0c;这不仅是对传统无线传输技术的一次革新&#xff0c…

父子Shell你了解多少?一起解读吧

一.source和点、bash \sh 、./script区别 1.source和点&#xff0c;执行脚本&#xff0c;只在当前shell环境中执行生效 2.指定bash\sh 解释器运行脚本&#xff0c;是开启subshell&#xff0c;开启子shell运行脚本 命令 3. ./script,都会指定shebang,通过解释器运行&#xff0c;…

PAT甲级-1090 Highest Price in Supply Chain

题目 题目大意 一个供应链由供应商、经销商、零售商组成。供应商作为根节点&#xff0c;售卖价格为P的商品&#xff0c;每经过一级经销商或零售商都会以高于r%的价格批发或出售。题目给出总节点数n&#xff0c;每个节点的编号从0到n-1&#xff0c;给出的每个值是该节点编号的索…

臀部筋膜炎最佳治疗方法

臀部筋膜炎的最佳治疗方法因个体差异而异&#xff0c;但通常包括以下几个方面&#xff1a; 一、药物治疗 非甾体抗炎药&#xff1a;如布洛芬、双氯芬酸钠等&#xff0c;这些药物通过抑制前列腺素合成来减少炎症和疼痛&#xff0c;适用于缓解轻至中度的急性发作期臀部筋膜炎引…

跨平台数据库工具DataGrip v2024.2全新发布——增加智能刷新功能

DataGrip 是一个跨平台的数据库工具可在Windows&#xff0c;OS X 和 Linux上使用。同时支持多种数据库&#xff0c;包含了SQL Server&#xff0c;Oracle&#xff0c;PostgreSQL&#xff0c;MySQL&#xff0c;DB2&#xff0c;Sybase&#xff0c;SQLite&#xff0c;Derby&#xf…

智慧农业的引擎:高标准农田灌区信息化的探索与实践

在现代农业的广阔图景中&#xff0c;智慧农业作为一股革新力量&#xff0c;正逐步重塑着传统农业的面貌。其中&#xff0c;高标准农田灌区的信息化建设不仅是智慧农业的重要引擎&#xff0c;更是实现农业可持续发展、提高资源利用效率的关键路径。 高标准农田灌区信息化的内涵…

828华为云征文|华为云Flexus云服务器X实例 基于CentOS系统镜像快速部署Laravel开源论坛

最近公司可热闹了&#xff01;大家都在为搭建博客论坛系统忙得不可开交&#xff0c;尤其是在选服务器这件事儿上&#xff0c;那叫一个纠结。 同事 A 说&#xff1a;“咱得选个厉害的服务器&#xff0c;不然这论坛以后卡得跟蜗牛爬似的可咋办&#xff1f;” 同事 B 回应道&#…

C++11语法(基础)【一】

目录 1. C11简介 2. 统一的列表初始化 2.1 &#xff5b;&#xff5d;初始化 2.2 std::initializer_list 3. 声明 3.1 auto 3.2 decltype 3.3 nullptr 声明&#xff1a;C11我会分几篇来讲&#xff0c;每一篇我都会讲几种特性。 1. C11简介 在2003年C标准委员会曾经提交了一份技术…

slam入门学习笔记

SLAM是Simultaneous localization and mapping缩写&#xff0c;意为“同步定位与建图”&#xff0c;主要用于解决机器人在未知环境运动时的定位与地图构建问题&#xff0c;目前广泛用于机器人定位导航领域&#xff0c;VR/AR方面&#xff0c;无人机领域&#xff0c;无人驾驶领域…

【小白请绕道】Redis 的 I/O 多路复用技术,它是如何工作的?

Redis 的 I/O 多路复用技术是其高性能的关键之一。在单个线程中&#xff0c;Redis 可以同时处理多个网络连接&#xff0c;这是通过使用 I/O 多路复用技术实现的。这种技术允许 Redis 在单个线程中监听多个套接字&#xff0c;并在套接字准备好执行操作时&#xff08;如读取或写入…

STM32F1,F4,L1系列禁止JTAG和SW引脚方法

STM32F1系列 程序中在使用到JTAG、SWD的某个IO 时&#xff0c;需要禁用掉相关调试方法后&#xff0c;再配置相应的IO方式。在需要相应的接口配置前使用这些代码。 对于F1系列&#xff0c;调用函数进行专门的禁止。 标准库配置方式&#xff1a; RCC_APB2PeriphClockCmd(RCC_A…

2024源代码加密软件TOP10分享|企业源代码加密软件

在现代企业的数字化转型过程中&#xff0c;源代码作为企业核心知识产权之一&#xff0c;至关重要。为了防止数据泄漏、外部攻击以及内部违规操作&#xff0c;企业越来越关注源代码的加密和保护。本文将为大家介绍2024年最受欢迎的十大源代码加密软件&#xff0c;帮助企业更好地…

助力新能源汽车行业的发展,尽在AUTO TECH 2025华南展

随着全球对环境保护的重视和石油资源的逐渐减少&#xff0c;新能源汽车的发展已经成为必然趋势。预计未来几年&#xff0c;新能源汽车的市场规模和销量将继续保持快速增长。根据 IDC 预测&#xff0c;中国乘用车市场中新能源车市场规模将在 2028 年超过 2300 万辆&#xff0c;年…

面试经典 150 题:力扣88. 合并两个有序数组

每周一道算法题启动 题目 【题目链接】 【解法一】合并后排序 排序后的数组自动省略0的数字&#xff0c;又学到了 class Solution { public:void merge(vector<int>& nums1, int m, vector<int>& nums2, int n) {//合并两个数组后排序for(int i0; i<…

基于springboot渔具销售系统设计与开发

文未可获取一份本项目的java源码和数据库参考。 选题背景及意义 随着社会的发展,渔具销售企业之间的竞争与合作变得越来越频繁.而销售部门作为企业的窗口,其地位无与伦比。在激烈的市场竞争中,企业要能对市场变化作出反应,销售部门起了关键作用,销售部门作为企业的生命已经成了…