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

【C++】多态的认识和理解

在这里插入图片描述
个人主页

在这里插入图片描述

文章目录

  • ⭐一、多态的概念
  • 🎄二、多态的定义及实现
    • 1.多态的构成
    • 2.实现多态的条件
    • 3.虚函数的概念
    • 4.虚函数的重写和覆盖
    • 5.析构函数的重写
    • 6.协变
    • 7.override和 final关键字
    • 8.重载、重写/覆盖、隐藏这三者的区别
  • 🏠三、纯虚函数和抽象类的关系
  • 🏝️四、多态的原理
    • 1.虚函数表指针
    • 2.多态是如何实现的
    • 3.动态绑定和静态绑定
  • 🚀五、虚函数表
    • 1.概念
    • 2.虚函数和虚函数表两者的存储位置

⭐一、多态的概念

多态(polymorphism),简单的来说就是多种形态。但多态又可以分为两种:一种是编译是的多态,称为静态多态;还有一种是运行时的多态,称为动态多态。下面我们就更进一步去了解一下什么是静态多态和什么是动态多态。

1.静态多态
静态多态主要就是函数重载以及函数模板,它们传不同的类型参数就可以调用不同的函数,通过参数不同就能够达到多种形态。而至于为什么会被称为是编译时的多态,是因为它们的实参传递给形参的参数匹配是在编译时完成的。

2.动态多态
具体的来说,就是去完成某个行为(函数),可以通过传不同的对象去完成不同的行为,因此达到多种形态。 例如:当我们要去买票时,我们会发现普通人去买票时,是全价票;学生去买票时,是优惠票;而军人去买票时是优先买票。又或者同样是动物叫这一行为,传猫对象过去,就是”(>ω<)喵“,传狗对象过去,就是"汪汪"。

🎄二、多态的定义及实现

1.多态的构成

多态是⼀种继承关系的下的类对象,去调用同一函数,从而产生了不同的行为。
例如:Student继承了Person,Person对象买票为全价票,而Student对象买票则为优惠票。

2.实现多态的条件

实现多态有两个重要的条件:
• 必须是指针或者引用调用的函数。
• 被调用的函数必须是虚函数。

如果想要实现多态的效果,首先必须是基类的指针或引用,因为只有基类的指针或引用才能指向派生类对象。其次派生类必须对基类的虚函数进行重新或者覆盖,派生类才能有不同的函数,从而达到多态的效果。

3.虚函数的概念

虚函数,就是在类成员函数前面加上virtual修饰,那么这个成员函数就被称为虚函数。注意:非成员函数不能加上virtual进行修饰。

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

4.虚函数的重写和覆盖

概念:在派生类中有一个跟基类完全相同的虚函数(既派生类的虚函数与基类的虚函数的返回值类型、函数名以及参数列表完全相同),称派生类的虚函数重写了基类的虚函数。
注意:在重写基类的虚函数时,派生类的虚函数在不加上virtual的情况下,也能构成重写(因为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但这种写法不是很规范,不建议这样使用。

class Person
{
public:virtual void BuyTicket(){cout << "买票-全价" << endl;}
};class Student : public Person {
public:virtual void BuyTicket() { cout << "买票-打折" << endl; }
};void Func(Person* ptr)
{// 这⾥可以看到虽然都是Person指针Ptr在调⽤BuyTicket// 但是跟ptr没关系,⽽是由ptr指向的对象决定的。ptr->BuyTicket();
}int main()
{Person ps;Student st;Func(&ps);Func(&st);return 0;
}

在这里插入图片描述

通过上述代码以及运行结果,我们可以发现子类Student中的BuyTicket重写了基类Person的BuyTicket。

5.析构函数的重写

我们首先要了解虽然析构函数的名字看起来不一样,但实际上编译器对析构函数的名称做了特殊的处理,编译后的析构函数名称统一处理为destructor,因此析构函数的名称实际上都为destructor。

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

通过阅读上述代码,如果我们在~A之前不加virtual是否会发生报错呢?结果是肯定的,因为如果 ~A()前面不加上virtual,那么deletep2时只调用了A的析构函数,而没用调用B的析构函数,从而导致内存泄漏的问题,这就是为什么在基类中的析构函数设计为虚函数。

6.协变

协变就是派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用派生类虚函数返回派生类对象的指针或者引用时,称为协变。

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

在这里插入图片描述

7.override和 final关键字

C++对函数的重写要求比较严格,如函数名字写错了或参数写错等都无法构成重载,而这种错误在编译期间是不会报出的,只有在运行时才会出现错误。因此在C++11中提供了override,帮助用户检测是否完成重写。如果我们不想让派生类重写这个虚函数,那么就可以用final进行修饰。

1.override

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

在这里插入图片描述
2.final

class Car
{
public:virtual void Drive() final {}
};
class Benz :public Car
{
public:virtual void Drive() { cout << "Benz-舒适" << endl; }
};
int main()
{return 0;
}

在这里插入图片描述

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

在这里插入图片描述

🏠三、纯虚函数和抽象类的关系

在虚函数的后面写上 =0 ,则这个函数就被称为纯虚函数。纯虚函数不需要定义实现(因为要被派生类进行重写),只需要声明即可。而包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象。如果派生类继承后不重写纯虚函数,那么派生类也是抽象类。 因此纯虚函数在某种程度上强制了派生类重写虚函数,因为不重写实例化不出对象。

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

🏝️四、多态的原理

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放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。⼀个含有虚函数的类中都至少都有⼀个虚函数表指针,因为⼀个类所有虚函数的地址要被放到这个类对象的虚函数表中,虚函数表也简称虚表

2.多态是如何实现的

我们以买票为例来探讨多态的实现。
在这里插入图片描述

通过下图我们可以看到,当满足多态条件后,底层不再是编译时通过调用对象来确定函数的地址,而是运行时到通过指向的对象的虚表中去确定对应的虚函数的地址,这样就实现了指针或引用指向基类去调用基类的虚函数,指向派生类去调用派生类对应的虚函数。
第⼀张图,ptr指向的Person对象,调用的是Person的虚函数;第⼆张图,ptr指向的Student对象,调用的是Student的虚函数。

在这里插入图片描述
在这里插入图片描述

3.动态绑定和静态绑定

不满足多态条件(指针或者引用+调用虚函数)的函数调用是在编译时绑定,也就是编译时确定调用函数的地址,叫做静态绑定。
在这里插入图片描述

满足多态条件的函数调用是在运行时绑定,也就是在运行时到指向对象的虚函数表中找到调用函数的地址,也就做动态绑定。
在这里插入图片描述

🚀五、虚函数表

1.概念

虚函数表本质是⼀个存虚函数指针的指针数组,⼀般情况这个数组最后面放了⼀个0x00000000标记。(这个C++并没有进行规定,各个编译器自行定义的,vs系列编译器会再后面放个0x00000000标记,g++系列编译不会放)

基类对象的虚函数表中存放基类所有虚函数的地址。

派生类的虚函数表中包含基类的虚函数地址,派生类重写的虚函数地址以及派生类自己的虚函数地址三个部分。

派生类由两部分构成,分别是继承下来的基类和自己的成员,⼀般情况下,继承下来的基类中有虚函数表指针,自己就不会再生成虚函数表指针。但是要注意的是这里继承下来的基类部分虚函数表指针和基类对象的虚函数表指针不是同⼀个,就像基类对象的成员和派生类对象中的基类对象成员也独立的。

2.虚函数和虚函数表两者的存储位置

虚函数和普通函数⼀样的,编译好后是⼀段指令,都是存在代码段的,只是虚函数的地址又被存放到了虚表中。

虚函数表的存储位置在C++标准中并没有规定,取决于不同的编译器,在vs中,虚函数表是存放在代码段(常量区)的。

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 大数据-133 - ClickHouse 基础概述 全面了解
  • 【Spring】面试题:Spring,Spring Boot, Spring MVC 的关系以及区别(5)
  • flutter集成百度地图定位 ‘BMKLocationManager.h‘ file not found报错
  • 《Learning to Prompt for Vision-Language Models》CoOp论文中文校对版
  • C++_类和对象(下篇)—— 内部类、匿名对象、对象拷贝时的编译器优化
  • Linux进阶 修改文件所在组
  • 【C++二叉树】JZ36 二叉搜索树与双向链表
  • 屏幕录制电脑,推荐使用这些录制方式
  • 【Python报错已解决】 Requests.exceptions.ProxyError: HTTPSConnectionPool
  • Python 实现Excel XLS和XLSX格式相互转换
  • 需要申请 TAC
  • 接口自动化测试框架实战(Pytest+Allure+Excel)
  • Renesas R7FA8D1BH (Cortex®-M85)内部RTC的应用
  • vue 2表格滚动加载
  • 心觉:成功学就像一把刀,有什么作用关键在于使用者(一)
  • 【跃迁之路】【519天】程序员高效学习方法论探索系列(实验阶段276-2018.07.09)...
  • conda常用的命令
  • CSS中外联样式表代表的含义
  • ES6--对象的扩展
  • HTTP请求重发
  • Idea+maven+scala构建包并在spark on yarn 运行
  • Invalidate和postInvalidate的区别
  • JavaScript 无符号位移运算符 三个大于号 的使用方法
  • Kibana配置logstash,报表一体化
  • LeetCode29.两数相除 JavaScript
  • mysql外键的使用
  • Python代码面试必读 - Data Structures and Algorithms in Python
  • 关于Flux,Vuex,Redux的思考
  • 诡异!React stopPropagation失灵
  • 计算机常识 - 收藏集 - 掘金
  • 紧急通知:《观止-微软》请在经管柜购买!
  • 快速体验 Sentinel 集群限流功能,只需简单几步
  • 深度解析利用ES6进行Promise封装总结
  • 一个SAP顾问在美国的这些年
  • 用Visual Studio开发以太坊智能合约
  • 自动记录MySQL慢查询快照脚本
  • AI算硅基生命吗,为什么?
  • ​一些不规范的GTID使用场景
  • #【QT 5 调试软件后,发布相关:软件生成exe文件 + 文件打包】
  • #进阶:轻量级ORM框架Dapper的使用教程与原理详解
  • $.extend({},旧的,新的);合并对象,后面的覆盖前面的
  • (非本人原创)史记·柴静列传(r4笔记第65天)
  • (理论篇)httpmoudle和httphandler一览
  • (力扣)循环队列的实现与详解(C语言)
  • (十一)c52学习之旅-动态数码管
  • (转)程序员疫苗:代码注入
  • (转)我也是一只IT小小鸟
  • ****三次握手和四次挥手
  • .bat批处理(五):遍历指定目录下资源文件并更新
  • .NET / MSBuild 扩展编译时什么时候用 BeforeTargets / AfterTargets 什么时候用 DependsOnTargets?
  • .NET C# 使用GDAL读取FileGDB要素类
  • .net core 外观者设计模式 实现,多种支付选择
  • .NETCORE 开发登录接口MFA谷歌多因子身份验证
  • .NET命令行(CLI)常用命令
  • .Net中间语言BeforeFieldInit