C++之多态的深度剖析

目录

前言

1.多态的概念

2.多态的定义及实现

2.1多态的构成条件

2.1.1重要条件

2.1.2 虚函数

2.1.3 虚函数的重写/覆盖

2.1.4 选择题

2.1.5 虚函数其他知识

协变(了解)

 析构函数的重写

override 和 final关键字

3. 重载,重写,隐藏的对比

 4.纯虚函数和抽象类

结束语


前言

在前面我们对C++的封装,继承等特性都有了了解和学习,接下来我们将对C++的第三大特性-多态进行认识和掌握。内容分为来两大部分,第一个是对多态的认识和运用,第二大部分是对多态原理的了解和扩展。

1.多态的概念

多态(Polymorphism)是面向对象编程(OOP)中的一个核心概念,它指的是同一个行为具有多个不同表现形式或形态的能力。在编程中,多态通常通过继承(inheritance)和接(interfaces来实现。

以下是多态的几个主要方面:

  1. 编译时多态(静态多态):这是在编译时确定的多态性,通常通过函数重载(function overloading)和模板(templates)来实现。编译器根据函数的参数类型或数量来决定调用哪个函数。

  2. 运行时多态(动态多态):这是在程序运行时确定的多态性,主要通过虚函数(virtual functions)和继承来实现。在运行时,根据对象的实际类型来调用相应的成员函数。

之所以叫编译时多态,是 因为他们实参传给形参的参数匹配是在编译时完成的,我们把编译时一般归为静态,运行时归为动态。
运行时多态,具体点就是去完成某个行为(函数),可以传不同的对象就会完成不同的行为,就达到多种 形态。比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是优惠买票(5折或75折);军人买票时是优先买票。再比如,同样是动物叫的一个行为(函数),传猫对象过去,就是”(>^ω^<)喵“,传狗对象过去,就是"汪汪"。

多态的关键特性包括:

  • 继承:子类继承父类的属性和行为,可以对这些行为进行重写(override)。
  • 虚函数:在基类中声明为虚的成员函数,可以在派生类中被重写,使得通过基类指针或引用调用函数时,能够根据对象的实际类型来调用相应的函数版本。
  • 虚函数表:用于实现运行时多态的数据结构,它存储了虚函数的地址,使得程序能够在运行时确定调用哪个函数。
  • 向上转型:将派生类对象的引用或指针转换为基类类型的引用或指针,这是多态实现的基础。

2.多态的定义及实现

2.1多态的构成条件

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

2.1.1重要条件

被调用的函数必须是虚函数
指针或者引用调用虚函数
说明:要实现多态效果,第一必须是基类的指针或引用,因为只有基类的指针或引用才能既指向派生 类对象;第二派生类必须对基类的虚函数重写/覆盖,重写或者覆盖了,派生类才能有不同的函数,多 态的不同形态效果才能达到。
ac30984662c6415b9d1e6519bc6a0a3c.png

2.1.2 虚函数

类成员函数前面加virtual修饰,那么这个成员函数被称为虚函数。注意⾮成员函数不能加virtual修
饰。
class Person {
public:virtual void BuyTicket() {cout << "买票全额" << endl;}
};

2.1.3 虚函数的重写/覆盖

虚函数的重写/覆盖: 派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表(类型,数量)完全相同),称派生类的虚函数重写了基类的虚函数。
注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承 后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样 使用,不过在考试选择题中,经常会故意买这个坑,让判断是否构成多态

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class Person {
public:virtual void BuyTicket() {cout << "买票全额" << endl;}
};
class Student : public Person {
public:virtual void BuyTicket() {cout << "学生票半价" << endl;}
};
//引用调用
void func(Person& p) {p.BuyTicket();
}
//指针调用
void func1(Person* p) {p->BuyTicket();// 这⾥可以看到虽然都是Person指针Ptr在调⽤BuyTicket// 但是跟ptr没关系,⽽是由ptr指向的对象决定的。
}
int main() {Person p1;Student s1;Person* p2 = new Person();Student* s2 = new Student();func(p1);func(s1);p1.BuyTicket();s1.BuyTicket();func1(&p1);func1(&s1);p2->BuyTicket();s2->BuyTicket();return 0;
}

 0a05807e075c4caca7e97d76458d4218.png

void func(Student& p) {
    p.BuyTicket();
}
//指针调用
void func1(Student* p) {
    p->BuyTicket();
}

如果改成Student,就会出问题,就不是多态了,也就不能传Person对象了。 

#include <iostream>
using namespace std;class Pet {
public:virtual void eat() const{cout << "Eat food" << endl;}
};
class Dog : public Pet{
public:virtual void eat() const {cout << "Dog eats meat!" << endl;}
};
class Cat :public Pet {
public:virtual void eat()const {cout << "Cat eats fish!" << endl;}
};
void func(const Pet& p) {p.eat();
}
int main() {Pet p;Dog g;Cat c;func(p);func(g);func(c);return 0;
}

 上述是宠物的一个多态实现。

这里我们测试一下,基类函数不加virtual会怎样,

class Pet {
public:
     void eat() const{
        cout << "Eat food" << endl;
    }
};

da905b0d2aef46c3b394655b042a457f.png

我们会发现多态效果没有实现,所以一定要加上virtual. 

2.1.4 选择题

下面程序输出结果是什么?(B)

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* p = new B; 创建了一个 B 类型的对象,并通过基类指针 p 指向它。
  • p->test(); 调用了 A 类的 test 方法(因为 B 类没有重写 test 方法)。
  • 在 A 类的 test 方法中,func(val) 被调用,没有指定 val 的值,因此它使用 A 类 func 方法的默认参数 1
  • 由于 func 是虚函数,并且 p 指向一个 B 类型的对象,所以 B 类的 func 方法被调用,接收到的参数是 1

2.1.5 虚函数其他知识

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

#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;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;
}
 析构函数的重写
基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同看起来不符合重写的规则,实际上编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor, 所以基类的析构函数加了vialtual修饰,派生类的析构函数就构成重写。

故在C++中,当一个基类的析构函数被声明为虚函数时,它确保了当通过基类指针或引用删除派生类对象时,会调用正确的析构函数,即派生类的析构函数,然后再调用基类的析构函数。这是因为虚析构函数允许动态绑定,确保了派生类对象被正确地销毁。

#include <iostream>
using namespace std;
class A {
public:virtual ~A() {cout << "delete A" << endl;}
}; 
class B :public A {public:~B() {cout << "~B()->delete:" << _p << endl;delete _p;}
protected:int* _p = new int[10];
};
int main() {A* a = new A;A* b = new B;delete a;delete b;return 0;
}

2c9ec88991b54a408d740498d87f66fc.png

当我们不把基类析构函数设置成virtual时, 会发现没有调用B的析构,该释放的资源没有释放掉。

public:~A() {cout << "delete A" << endl;}
}; 

e451daade64e44c1a890b5c6dbc382e7.png

故基类的析构函数我们要设置成虚函数。

override 和 final关键字
从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,比如函数名写错参数写错等导致无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失。
如果不想让派生类重写这个虚函数,那么可以用final去修饰。
在C++中,override 和 final 关键字是C++11标准引入的,用于增强类继承和虚函数的声明。

override 关键字用于明确指出一个成员函数旨在重写(覆盖)其基类中的一个虚函数。如果该函数没有正确地重写基类中的任何虚函数,编译器将报错。这有助于避免因拼写错误或参数列表不匹配而意外地没有重写虚函数的情况。
 
class Car {public:virtual void Dirve(){}};class Benz :public Car {public:virtual void Drive() override { cout << "Benz-舒适" << endl; }};

比如上面这个例子,函数名写错了,重写失败,编译报错。

3c24128dc270456985512ecfe80ec180.png


final 关键字用于防止类被进一步派生,或者防止虚函数被重写。当应用于类时,它表示这个类不能被继承。当应用于虚函数时,它表示这个虚函数不能在派生类中被重写。

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

 992bd20cde2c433b998df1a03c050a49.png



class Base final { // 不能从这个类派生其他类
public:
    virtual void doSomething() const final {} // 这个虚函数不能被重写
};
// 下面的类声明会导致编译错误,因为 Base 是 final 的
// class Derived : public Base {};
// 下面的函数声明也会导致编译错误,因为 doSomething 是 final 的
// class Derived : public Base {
// public:
//     void doSomething() const override {} // 错误:不能重写 final 函数
// };

使用 final 关键字可以确保类或虚函数的行为不会被意外的继承或重写改变,这对于设计那些不打算被扩展的类或函数非常有用。 

3. 重载,重写,隐藏的对比

重载(Overloading)

  • 定义:在同一作用域内,可以定义多个同名函数,只要它们的参数列表(参数的数量、类型或顺序)不同。
  • 特点
    • 发生在同一类中。
    • 参数列表必须不同。
    • 返回类型可以不同,但不是区分重载的主要因素。

重写(Overriding)

  • 定义:在派生类中提供一个与基类中虚函数同名、参数列表和返回类型相同的函数,以实现多态。
  • 特点
    • 发生在基类和派生类之间。
    • 参数列表和返回类型必须相同。
    • 基类函数必须是虚函数。
    • 使用 override 关键字可以明确指出重写意图。

 隐藏(Hiding)

  • 定义:在派生类中定义一个与基类中成员(非虚函数或非静态成员变量)同名的成员,导致基类中的同名成员在派生类中不可见。
  • 特点
    • 发生在基类和派生类之间。
    • 可以是函数或变量。
    • 如果是函数,参数列表不必相同。
    • 如果派生类中的成员与基类中的成员具有相同的名称,但不同的参数列表,则基类成员被隐藏,而不是重载或重写。

 4.纯虚函数和抽象类

在虚函数的后面写上 =0 ,则这个函数为纯虚函数,纯虚函数不需要定义实现(实现没啥意义因为要被派生类重写,但是语法上可以实现),只要声明即可。
包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象,如果派生类继承后不重写纯虚函数,那么派生类也是抽象类。纯虚函数某种程度上强制了派生类重写虚函数,因为不重写实例化不出对象。
#include <iostream>
using namespace std;
class Car {
public:virtual void Drive() = 0;};
class Benchi :public Car {
public:virtual void Drive() {cout << "Benchi-舒适" << endl;}
};class Baoma :public Car {
public:virtual void Drive() {cout << "Baoma-上手" << endl;}
};
int main() {Car car;Car* b = new Benchi();b->Drive();Car* m = new Baoma();m->Drive();return 0;
}

337ab3ba1fc946b4ba2ffe85b6eb524f.png

 这里Car是抽象类,所以无法实例化对象。

结束语

本期内容就到此结束了,内容有点多,下节我们将对多态的原理进行补充讲解。

最后感谢各位友友的支持!!!

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

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

相关文章

如何从iconfont中获取字体图标并应用到微信小程序中去?

下面我们一一个微信小程序的登录界面的制作为例来说明&#xff0c;如何从iconfont中获取字体图标是如何应用到微信小程序中去的。首先我们看效果。 这里所有的图标&#xff0c;都是从iconfont中以字体的形式来加载的&#xff0c;也就是说&#xff0c;我们自始至终没有使用一张…

Linux shell编程学习笔记87:blkid命令——获取块设备信息

0 引言 在进行系统安全检测时&#xff0c;我们需要收集块设备的信息&#xff0c;这些可以通过blkid命令来获取。 1 blkid命令的安装 blkid命令是基于libblkid库的命令行工具&#xff0c;可以在大多数Linux发行版中使用。 如果你的Linux系统中没有安装blkid命令&#xff0c;…

RuoYi-Vue 使用开发 人员管理-查询功能

说明&#xff1a;这里仅仅开发列表显示 与 查询功能&#xff0c;剩下的添加、修改等可能会遇到报错&#xff0c;后面有机会&#xff0c;会单独写一篇文章教学处理 1.了解开发需求 作为示例的二级开发&#xff0c;这里的人员管理&#xff0c;管理的是 部门信息&#xff0c;员工…

Tomcat 11 下载/安装 与基本使用

为什么要使用Tomcat&#xff1f; 使用Apache Tomcat的原因有很多&#xff0c;以下是一些主要的优点和特点&#xff1a; 1. 开源与免费 Tomcat是一个完全开源的项目&#xff0c;任何人都可以免费使用。它由Apache软件基金会维护&#xff0c;拥有一个活跃的社区&#xff0c;这…

Django入门教程——用户管理实现

第六章 用户管理实现 教学目的 复习数据的增删改查的实现。了解数据MD5加密算法以及实现模型表单中&#xff0c;自定义控件的使用中间件的原理和使用 需求分析 系统问题 员工档案涉及到员工的秘密&#xff0c;不能让任何人都可以看到&#xff0c;主要是人事部门进行数据的…

[ 问题解决篇 ] 解决远程桌面安全登录框的问题

&#x1f36c; 博主介绍 &#x1f468;‍&#x1f393; 博主介绍&#xff1a;大家好&#xff0c;我是 _PowerShell &#xff0c;很高兴认识大家~ ✨主攻领域&#xff1a;【渗透领域】【数据通信】 【通讯安全】 【web安全】【面试分析】 &#x1f389;点赞➕评论➕收藏 养成习…

微信小程序时间弹窗——年月日时分

需求 1、默认当前时间2、选择时间弹窗限制最大值、最小值3、每次弹起更新最大值为当前时间&#xff0c;默认值为上次选中时间4、 minDate: new Date(2023, 10, 1).getTime(),也可以传入时间字符串new Date(2023-10-1 12:22).getTime() html <view class"flex bb ptb…

【Spring框架】Spring框架的开发方式

目录 Spring框架开发方式前言具体案例导入依赖创建数据库表结构创建实体类编写持久层接口和实现类编写业务层接口和实现类配置文件的编写 IoC注解开发注解开发入门&#xff08;半注解&#xff09;IoC常用注解Spring纯注解方式开发 Spring整合JUnit测试 Spring框架开发方式 前言…

江协科技STM32学习- P24 DMA数据转运DMA+AD多通道

&#x1f680;write in front&#x1f680; &#x1f50e;大家好&#xff0c;我是黄桃罐头&#xff0c;希望你看完之后&#xff0c;能对你有所帮助&#xff0c;不足请指正&#xff01;共同学习交流 &#x1f381;欢迎各位→点赞&#x1f44d; 收藏⭐️ 留言&#x1f4dd;​…

【刷题11】CTFHub技能树sql注入系列

整数型注入 看到源码了&#xff0c;直接sql一套秒了 字符型注入 SQL 报错注入 构造payload 1 and (select extractvalue(1,concat(’~’,(select database())))) 后续步骤跟sql基本步骤一样 SQL 布尔注入 人工测试太麻烦&#xff0c;这里直接使用sqlmap,知道这有sql注入漏洞&am…

面试经典 150 题.P26. 删除有序数组中的重复项(003)

本题来自&#xff1a;力扣-面试经典 150 题 面试经典 150 题 - 学习计划 - 力扣&#xff08;LeetCode&#xff09;全球极客挚爱的技术成长平台https://leetcode.cn/studyplan/top-interview-150/ 题解&#xff1a; class Solution {public int removeDuplicates(int[] nums) …

docker中使用ros2humble的rviz2不显示问题

这里写目录标题 docker中使用ros2humble的rviz2不显示问题删除 Docker 镜像和容器删除 Docker 容器Linux服务器下查看系统CPU个数、核心数、(make编译最大的)线程数总结&#xff1a; RVIZ2 不能显示数据集 docker中使用ros2humble的rviz2不显示问题 问题描述&#xff1a; roo…

ELK + Filebeat + Spring Boot:日志分析入门与实践(二)

目录 一、环境 1.1 ELKF环境 1.2 版本 1.3 流程 二、Filebeat安装 2.1 安装 2.2 新增配置采集日志 三、logstash 配置 3.1 配置输出日志到es 3.2 Grok 日志格式解析 3.2 启动 logstash ​3.3 启动项目查看索引 一、环境 1.1 ELKF环境 springboot项目&#xff1a;w…

基于SSM土家风景文化管理系统的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;用户管理&#xff0c;景点分类管理&#xff0c;热门景点管理&#xff0c;门票订单管理&#xff0c;旅游线路管理&#xff0c;系统管理 前提账号功能包括&#xff1a;系统首页&#xff0c;个人中心&…

Linux特种文件系统--tmpfs文件系统

tmpfs类似于RamDisk&#xff08;只能使用物理内存&#xff09;&#xff0c;使用虚拟内存&#xff08;简称VM&#xff09;子系统的页面存储文件。tmpfs完全依赖VM&#xff0c;遵循子系统的整体调度策略。说白了tmpfs跟普通进程差不多&#xff0c;使用的都是某种形式的虚拟内存&a…

不同概率分布的更新过程——Python实现(均匀分布、卡方分布、指数分布等作为概率分布的更新过程)

更新过程(renewal process)是描述元件或设备更新现象的一类随机过程。以下是对更新过程的详细介绍: 一、定义与特点 定义:设对某元件的工作进行观测,假定元件的使用寿命是一随机变量,当元件发生故障时就进行修理或换上新的同类元件,而且元件的更新是即时的(修理或更换…

GIT分布式版本控制系统基础操作

问题大纲 1、什么分布式版本控制系统 2、简述Git的使用分为哪几个步骤 3、克隆和拉取的区别是什么&#xff1f; 4、git相关的所有指令 一、分布式版本控制系统 分布式版本控制系统是一种版本控制系统&#xff0c;它允许每个用户都拥有完整的项目历史记录和版本控制信息。与…

ArcGIS必会的选择要素方法(AND、OR、R、IN等)位置选择等

今天来看看ArcGIS中的几个选择的重要使用方法 1、常规选择、 2、模糊查询、 3、组合复合条件查询&#xff08;AND、OR、IN&#xff09;&#xff0c; 4、空值NULL查询 5、位置选择 推荐学习&#xff1a; 以梦为马&#xff0c;超过万名学员学习ArcGIS入门到实战的应用课程…

Spring Bean创建流程

Spring Bean 创建流程图 大家总是会错误的理解Bean的“实例化”和“初始化”过程&#xff0c;总会以为初始化就是对象执行构造函数生成对象实例的过程&#xff0c;其实不然&#xff0c;在初始化阶段实际对象已经实例化出来了&#xff0c;初始化阶段进行的是依赖的注入和执行一…

rtp协议:rtcp包格式和传输间隔

RTP Control Protocol -- RTCP-rtp控制协议 实时传输控制协议&#xff08;RTCP&#xff09;基于对会话中的所有参与者定期传输控制包&#xff0c;使用与数据包相同的分发机制。底层协议必须提供数据包和控制包的多路复用&#xff0c;例如使用UDP时使用不同的端口号。RTCP执行四…