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.mzph.cn/pingmian/58301.shtml

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

相关文章

Codeforces Round 919 (Div. 2)

B. Summation Game 题意 输入 输出 思路 遇到两人博弈问题&#xff0c;可以分别贪心&#xff0c;先贪心一个固定下来&#xff0c;然后遍历贪心另一个人 void solve() {int n, k, x;cin >> n >> k >> x;vector<int> arr(n 1);int *s new int[n …

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

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

【linux网络编程】| socket套接字 | 实现UDP协议聊天室

前言&#xff1a;本节内容将带友友们实现一个UDP协议的聊天室。 主要原理是客户端发送数据给服务端。 服务端将数据再转发给所有链接服务端的客户端。 所以&#xff0c; 我们主要就是要实现客户端以及服务端的逻辑代码。 那么&#xff0c; 接下来开始我们的学习吧。 ps:本节内容…

vivo 轩辕文件系统:AI 计算平台存储性能优化实践

在早期阶段&#xff0c;vivo AI 计算平台使用 GlusterFS 作为底层存储基座。随着数据规模的扩大和多种业务场景的接入&#xff0c;开始出现性能、维护等问题。为此&#xff0c;vivo 转而采用了自研的轩辕文件系统&#xff0c;该系统是基于 JuiceFS 开源版本开发的一款分布式文件…

Java | Leetcode Java题解之第520题检测大写字母

题目&#xff1a; 题解&#xff1a; class Solution {public boolean detectCapitalUse(String word) {// 若第 1 个字母为小写&#xff0c;则需额外判断第 2 个字母是否为小写if (word.length() > 2 && Character.isLowerCase(word.charAt(0)) && Charact…

如何封装一个可取消的 HTTP 请求?

前言 你可能会好奇什么样的场景会需要取消 HTTP 请求呢&#xff1f; 确实在实际的项目开发中&#xff0c;可能会很少有这样的需求&#xff0c;但是不代表没有&#xff0c;比如&#xff1a; 假如要实现上述这个公告栏&#xff0c;每点击一个 tab 按钮就会切换展示容器容器中…

图的最短路径算法-迪杰斯特拉(Dijkstra)算法与弗洛伊德(Frolyd)算法(更新中)

一、最短路径算法&#xff08;Shortest Path&#xff09; 最短路径问题是图论研究中的一个经典算法问题&#xff0c;旨在寻找图&#xff08;由结点和路径组成的&#xff09;中两结点之间的最短路径。 最短路径不一定是经过边最少的路径&#xff0c;但在这些最短路径中&#x…

ubuntu编译ffmpeg

配置 运行环境&#xff1a;vmware ubuntu 20.04 时间&#xff1a;2024年10月24日 权限问题&#xff1a;由于ubuntu权限问题 建议使用root权限编译&#xff0c;且~是根据用户组来进行定位的。 环境配置更新 cd ~ && \ mkdir ffmpeg_sources ffmpeg_build bin &…

EasyExcel自定义下拉注解的三种实现方式

文章目录 一、简介二、关键组件1、ExcelSelected注解2、ExcelDynamicSelect接口&#xff08;仅用于方式二&#xff09;3、ExcelSelectedResolve类4、SelectedSheetWriteHandler类 三、实际应用总结 一、简介 在使用EasyExcel设置下拉数据时&#xff0c;每次都要创建一个SheetWr…

【vs2022】windows可用的依赖预编译库

ffmpeg 、x264 、x265 等。obs是基于qt6+vs2022+64bit obs的官网传统构建已经不用了obs的s2022构建OBS Deps Build 2024-09-12FFmpeg4.4 库,x64 可用。

每天五分钟深度学习pytoroch:基于pytorch搭建逻辑回归算法模型

本文重点 前面我们学习了线性回归模型的搭建,无论是基于pytorch还是不基于pytorch,以上的模型都是回归模型,本文我们将使用pytorch搭建逻辑回归模型,逻辑回归模型是一个经典的分类问题。 模型搭建 class LogisticRegression(nn.Module) : def __init__(self) :super (Lo…

玩转Docker | 使用Docker部署推箱子网页小游戏

玩转Docker | 使用Docker部署推箱子网页小游戏 一、项目介绍项目简介项目预览 二、系统要求环境要求环境检查Docker版本检查检查操作系统版本 三、部署推箱子网页小游戏下载镜像创建容器检查容器状态检查服务端口安全设置 四、访问推箱子网页小游戏五、总结 一、项目介绍 项目…

Iperius Backup(数据备份软件) v8.3.0 中文免费版

下载&#xff1a; 【1】https://pan.quark.cn/s/19ef716c02d5 【2】https://drive.uc.cn/s/197acba8d8d94?public1 Iperius Backup是一款专业的备份还原软件&#xff0c;功能强大&#xff0c;支持DAT备份、LTO备份、NAS备份、磁带备份、RDX驱动器、USB备份&#xff0c;满足用…

K8S测试pod内存和CPU资源不足

只设置requests参数 mysql主从pod启动后监控 读压测之后 同时设置limits和requests&#xff0c;只调低内存值 监控 压力测试 同时设置limits和requests&#xff0c;只调低CPU值 初始状态 开始压测 结论 对于CPU&#xff0c;如果pod中服务使用CPU超过设置的limits&…

谷歌云GCP基础概念讲解

概览 云的基础是虚拟化&#xff1a;服务器&#xff0c;存储&#xff0c;网络。服务器是远程计算机的逻辑分区。存储是物理硬盘的逻辑划分。网络则是虚拟私有云。 谷歌是唯一一个拥有全球私有基础设施的公司&#xff1b;他们的谷歌云基础设施没有任何一部分通过公共互联网。换句…

Python 爬虫的寻宝大冒险:如何捕获 API 数据的宝藏

在这个信息爆炸的数字时代&#xff0c;数据就像是隐藏在网络深处的宝藏&#xff0c;等待着勇敢的探险家去发现。今天&#xff0c;我们要讲述的是如何成为一名 Python 爬虫探险家&#xff0c;装备你的代码工具&#xff0c;深入 API 的迷宫&#xff0c;捕获那些珍贵的数据宝藏。 …

C++,STL 051(24.10.28)

内容 1.map容器的构造函数。 2.map容器的赋值操作。 运行代码 #include <iostream> #include <map>using namespace std;void printMap(map<int, int> &m) {for (map<int, int>::iterator it m.begin(); it ! m.end(); it){cout << &quo…

《链表篇》---环形链表II(返回节点)

题目传送门 方法一&#xff1a;哈希表&#xff08;与环形链表类似&#xff09; 很容易就可以找到链表的相交位置。 public class Solution {public ListNode detectCycle(ListNode head) {if(head null || head.next null){return null;}Set<ListNode> visited new Ha…

从0开始深度学习(17)——数值稳定性和模型初始化

在每次训练之前&#xff0c;都会对模型的参数进行初始化&#xff0c;初始化方案的选择在神经网络学习中起着举足轻重的作用&#xff0c; 它对保持数值稳定性至关重要。 我们选择哪个函数以及如何初始化参数可以决定优化算法收敛的速度有多快。 糟糕选择可能会导致我们在训练时遇…

重学SpringBoot3-怎样优雅停机

更多SpringBoot3内容请关注我的专栏&#xff1a;《SpringBoot3》 期待您的点赞&#x1f44d;收藏⭐评论✍ 重学SpringBoot3-怎样优雅停机 1. 什么是优雅停机&#xff1f;2. Spring Boot 3 优雅停机的配置3. Tomcat 和 Reactor Netty 的优雅停机机制3.1 Tomcat 优雅停机3.2 Reac…