【二十四】【C++】多态

多态的基本概念

多态是一种允许使用相同的接口来访问不同的底层形式(类型)的对象的能力。C++中的多态主要通过以下两种方式实现:

编译时多态(静态多态):通过函数重载运算符重载实现。

运行时多态(动态多态):通过虚函数继承实现。

编译时多态(静态多态)

编译时多态是在编译期决定的,主要通过函数重载和运算符重载来实现。

函数重载:在同一个作用域内,可以声明几个功能相似的同名函数,但是这些函数的参数类型和/或数量不同,编译器根据函数调用时的参数类型和数量来决定具体调用哪个函数。

运算符重载:允许定义或重新定义大部分C++内置的运算符作用于自定义类型的操作,这使得运算符可以根据操作数的类型来执行不同的操作。

运行时多态(动态多态)

运行时多态是在程序运行时决定的,主要依靠虚函数(virtual function)和继承机制来实现。

虚函数:在基类中用virtual关键字声明的函数,可以在派生类中被重写(override)。这样,基类指针或引用指向派生类对象时,调用的是派生类中的函数,这种机制称为动态链接或后期绑定。

纯虚函数:在基类中声明为纯虚函数(通过virtual 返回类型 函数名() = 0;的方式声明),这样的基类称为抽象基类,不能直接实例化。纯虚函数必须在派生类中被实现,除非派生类也是一个抽象类。

多态的使用

多态使得我们可以使用基类的指针或引用来操作不同的派生类对象,而具体调用哪个类的哪个方法,则是在运行时决定的。这种机制使得代码更加灵活和通用,易于扩展和维护。

 
#include <iostream>
using namespace std;class Shape {
public:virtual void draw() = 0; // 纯虚函数
};class Circle : public Shape {
public:void draw() override {cout << "Drawing Circle" << endl;}
};class Rectangle : public Shape {
public:void draw() override {cout << "Drawing Rectangle" << endl;}
};int main() {Shape* shape1 = new Circle();Shape* shape2 = new Rectangle();shape1->draw(); // 输出: Drawing Circleshape2->draw(); // 输出: Drawing Rectangledelete shape1;delete shape2;return 0;
}

在这个示例中,通过Shape类的指针调用draw()函数时,具体调用哪个版本的draw()(是Circle的还是Rectangle的),取决于指针实际指向的对象类型。这就是运行时多态的体现。

Shape类的指针,属于范围小的类型,在自己的地址上寻找相符合的信息部分。但是虚函数重写,导致虚基类中的draw函数变化,Shape类的指针在地址上找到的相符合的信息部分是被重写过的函数。

不使用多态的版本(隐藏)

 
#if 1
#include <iostream>
using namespace std;class Shape {
public:void draw() {cout << "Drawing Shape" << endl;};};class Circle : public Shape {
public:void draw()  {cout << "Drawing Circle" << endl;}};class Rectangle : public Shape {
public:void draw()  {cout << "Drawing Rectangle" << endl;}};int main() {Shape* shape1 = new Circle();Shape* shape2 = new Rectangle();shape1->draw(); shape2->draw(); return 0;}
#endif

Shape类的指针,属于范围小的类型,在自己的地址上寻找相符合的信息部分。派生类和基类有名字相同的成员,会发生隐藏,隐藏基类的成员,此时派生类有两个相同的成员,如果要访问基类成员需要使用作用域限定符。

虚函数

虚函数是C++中支持多态性的一种机制。它允许在派生类中重写基类的方法,使得通过基类指针或引用调用这些方法时,能够执行到派生类中相应的重写版本,实现运行时的多态性。这意味着在程序执行期间,可以根据对象的实际类型来决定调用哪个函数,而不是在编译时。

定义虚函数

在基类中,使用virtual关键字声明的成员函数就是虚函数。声明虚函数的目的是允许在派生类中对其进行重写(override)。

 
class Base {
public:virtual void show() {cout << "Base class show" << endl;}
};

在上面的代码中,show()函数是一个虚函数。

虚函数的重写

虚函数重写(Override)允许子类重新定义基类中的虚函数实现。这是实现多态性的关键机制之一。通过虚函数重写,可以在运行时根据对象的实际类型调用相应类的方法,而不是在编译时确定。

虚函数重写的实现

要实现虚函数重写,需要遵循以下几个步骤:

在基类中声明虚函数:首先,你需要在基类中使用 virtual 关键字声明一个虚函数。这表示该函数可以在任何派生类中被重写。

 
class Base {
public:virtual void show() {cout << "Base class show function called." << endl;}
};

这里我们声明了一个基类 Base,其中包含一个虚函数 show()。使用 virtual 关键字标记了 show() 函数,表示它可以在派生类中被重写。

在派生类中重写虚函数:在派生类中,你可以重写基类中声明的虚函数。重写时,函数的返回类型、名称以及参数列表必须与基类中声明的虚函数完全相同。

 
class Derived : public Base {
public:void show() override { // C++11引入了override关键字,确保了函数重写的正确性cout << "Derived class show function called." << endl;}
};

这里我们创建了一个 Derived 类,它从 Base 类继承。在 Derived 类中,我们重写了 show() 函数。使用 override 关键字(C++11引入)表明这是一个重写的函数,这有助于编译器检查函数签名的一致性,确保我们正确地重写了函数。

虚函数重写的特定要求

函数签名必须匹配:重写的虚函数必须与基类中的原始虚函数有相同的函数签名(即相同的返回类型、函数名称和参数列表)。

基类函数必须是虚函数:只有被声明为 virtual 的基类函数才可以被派生类重写。

使用 override 关键字(可使用,也可不使用,但推荐):在C++11及以后的版本中,可以在派生类的函数声明后使用 override 关键字。这不是必需的,但它可以帮助编译器检查派生类是否正确地重写了基类的虚函数。

协变

在C++中,协变主要应用于虚函数的返回类型。协变允许派生类中重写的虚函数拥有与基类虚函数不同的返回类型,但这两个返回类型必须保持一定的继承关系。

协变的基本规则

只适用于返回类型:协变仅适用于方法返回类型的变化。

派生关系:派生类中重写的虚函数的返回类型,必须是基类对应虚函数返回类型的派生类(或同类型,同类型不是协变)。

协变的实现

假设有一个基类 Base 和一个从 Base 派生的类 Derived,同时有一个返回 Base 类型指针的虚函数。在派生类中重写该虚函数时,可以返回 Derived 类型的指针,这就是协变的体现。

示例代码

 
class Base {
public:virtual Base* clone() const {// 实现克隆自己的逻辑return new Base(*this);}
};class Derived : public Base {
public:Derived* clone() const override {// 实现克隆自己的逻辑return new Derived(*this);}
};

基类 Base 有一个名为 clone 的虚函数,返回类型是指向 Base 类的指针。

派生类 Derived 重写了 clone 函数,并将返回类型改为指向 Derived 类的指针。这里展示了协变:返回类型从 Base* 变为了 Derived*

注意,这里 Derived::clone 函数正确地重写了基类中的 clone 函数,尽管返回类型不同。这是因为 Derived*Base* 的协变返回类型,且 DerivedBase 的派生类。

协变的特定要求和限制

只适用于返回类型:协变只能用于方法的返回类型。

继承关系:派生类方法的返回类型必须是基类方法返回类型的派生类型。

指针和引用:协变仅适用于返回类型为指针或引用的情况。对于返回具体对象的情况,不支持协变。

虚析构函数的重写

虚析构函数允许通过基类指针来正确地删除派生类对象。当一个基类声明了虚析构函数时,派生类的析构函数自动成为虚函数,无论是否显式地使用 virtual 关键字。虚析构函数确保了当通过基类指针删除派生类对象时,能够先调用派生类的析构函数,然后是基类的析构函数,从而正确地释放资源。

析构函数的重要性

如果基类的析构函数不是虚函数,则删除派生类对象时可能只会调用基类的析构函数,从而导致派生类分配的资源没有被正确释放,引发内存泄露。声明虚析构函数可以防止这种情况。

重写虚析构函数

 
/*虚析构函数*/
#if 1
#include <iostream>
using namespace std;
class Base {
public:virtual ~Base() {std::cout << "Base destructor called." << std::endl;}};class Derived : public Base {
public:~Derived() {std::cout << "Derived destructor called." << std::endl;}};int main(){Base* p1=new Base;Base* p2=new Derived;delete p1;delete p2;}#endif

基类 Base 有一个虚析构函数。使用 virtual 关键字确保了析构函数是虚拟的。

派生类 Derived 重写了析构函数。虽然没有显式使用 virtual 关键字,但由于基类的析构函数是虚的,派生类的析构函数自动成为虚函数。

派生类Derived 析构函数的调用顺序,是先调用派生类的析构函数再调用基类的虚构函数。

不重写析构函数

 
/*不重写析构函数*/
#if 1
#include <iostream>
using namespace std;
class Base {
public:~Base() {std::cout << "Base destructor called." << std::endl;}};class Derived : public Base {
public:~Derived() {std::cout << "Derived destructor called." << std::endl;}};int main(){Base* p1=new Base;Base* p2=new Derived;delete p1;delete p2;}#endif

析构函数的特定要求

正确的资源释放:当通过基类指针删除派生类对象时,虚析构函数确保派生类和基类的析构函数都会被正确调用,从而释放所有相关资源。顺序是先调用派生类的析构函数,再调用基类的析构函数。

基类应该有虚析构函数:如果一个类被设计为基类,并且预期它会被其他类继承,则应该声明一个虚析构函数,即使析构函数不执行任何操作。

注意点

虚析构函数的性能影响:虚函数(包括虚析构函数)可能会引入轻微的性能开销,因为它们需要通过虚表(vtable)来解析调用。但在绝大多数情况下,这种开销是可以接受的,特别是考虑到它带来的正确性和灵活性。

避免内存泄漏:即使派生类没有分配任何动态内存,如果基类有虚析构函数,派生类也应正确重写析构函数,以避免潜在的资源管理问题。

C++中 override 和 final 关键字

在C++中,overridefinal 是两个与虚函数重写相关的关键字,它们在C++11标准中引入。这两个关键字提供了额外的语义,帮助程序员更明确地表达他们的设计意图,同时也使得编译器能够提供更好的检查和优化。

override

当你在派生类中重写基类的虚函数时,可以在派生类的函数声明后面使用 override 关键字。这表明该函数旨在重写一个基类中的虚函数。如果声明的函数没有重写任何基类中的虚函数(比如由于函数签名不匹配),编译器将报错。这有助于捕捉到可能的错误,比如拼写错误或者错误的参数类型。

 
class Base {
public:virtual void func() {}
};class Derived : public Base {
public:void func() override {} // 正确重写// void fun() override {} // 编译错误,因为Base中没有fun()函数
};

在这个例子中,Derived 类通过 override 关键字标明它意图重写 Base 类中的 func() 函数。如果 Derived 类中的函数签名与任何基类中的虚函数都不匹配,编译器将报错。

final

final 关键字可以用于防止类被进一步派生或虚函数被进一步重写。当你将一个类标记为 final 时,任何尝试从这个类派生出新类的行为都会导致编译错误。同样,将虚函数标记为 final 会阻止任何派生类重写该函数。

 
class Base {
public:virtual void func() final {} // 防止进一步重写
};class Derived final : public Base { // 防止进一步派生
public:// void func() override {} // 编译错误,因为Base::func()被标记为final
};// class MoreDerived : public Derived {}; // 编译错误,因为Derived被标记为final

在这个例子中,Base 类中的 func() 被标记为 final,这意味着任何尝试在派生类中重写 func() 的操作都会导致编译错误。同时,Derived 类被标记为 final,防止任何进一步的派生。

使用 override 和 final 的好处

提高代码清晰度:使用 overridefinal 关键字可以让你的代码意图更加明确,使其他开发者更容易理解你的设计意图。

编译时检查:它们允许编译器在编译时进行额外的检查,捕捉潜在的错误,比如签名不匹配或错误地重写了不应该重写的函数。

优化支持:明确指出哪些函数不会被进一步重写,可以帮助编译器做出更好的优化决策。

重载、重写、重定义(隐藏)

重载 (Overloading)

重载指的是在相同作用域有两个或多个函数拥有相同的名称,但是它们的参数列表不同(参数类型、个数或者顺序不同)。重载使得函数可以根据不同的参数执行不同的任务。

  • 两个函数在同一作用域

  • 函数名相同

  • 参数列表不同

 
void print(int i) {std::cout << "Printing int: " << i << std::endl;
}void print(double f) {std::cout << "Printing float: " << f << std::endl;
}void print(const std::string &s) {std::cout << "Printing string: " << s << std::endl;
}

在这个例子中,print 函数被重载了三次,分别接受 intdoublestd::string 类型的参数。

重写 (Overriding)

重写是面向对象编程中的一个概念,指的是派生类中的函数重写了基类中具有相同名称相同参数列表相同返回值(或协变)的虚函数。重写用于实现运行时多态。

  • 两个函数分别在基类和派生类的作用域

  • 函数名、参数列表、返回值必须相同(协变、虚析构函数除外)

  • 两个函数必须是虚函数

 
class Base {
public:virtual void display() {std::cout << "Display Base class" << std::endl;}
};class Derived : public Base {
public:void display() override {  // 使用override确保正确重写std::cout << "Display Derived class" << std::endl;}
};

在这个例子中,Derived 类重写了 Base 类中的 display 函数。

重定义 (Hiding or Redefinition)

重定义发生在派生类中,当派生类定义了一个与基类同名的成员(不论参数列表是否相同),该成员会隐藏(或称为重定义)基类中所有同名的成员,不论其参数列表。这不是多态,而是名字隐藏。

  • 两个函数分别在基类和派生类的作用域

  • 函数名相同

  • 不构成重写

 
class Base {
public:void display() {std::cout << "Display Base class" << std::endl;}
};class Derived : public Base {
public:void display(int i) {  // 重定义,隐藏了基类的display()std::cout << "Display Derived class with int: " << i << std::endl;}
};

在这个例子中,尽管 Derived 类中的 display 函数的参数列表与基类中的不同,它仍然隐藏了基类中的 display() 函数。

小结论

重载

  • 两个函数在同一作用域

  • 函数名相同

  • 参数列表不同

重写

  • 两个函数分别在基类和派生类的作用域

  • 函数名、参数列表、返回值必须相同(协变、虚析构函数除外)

  • 两个函数必须是虚函数

重定义

  • 两个函数分别在基类和派生类的作用域

  • 函数名相同

  • 不构成重写

结尾

最后,感谢您阅读我的文章,希望这些内容能够对您有所启发和帮助。如果您有任何问题或想要分享您的观点,请随时在评论区留言。

同时,不要忘记订阅我的博客以获取更多有趣的内容。在未来的文章中,我将继续探讨这个话题的不同方面,为您呈现更多深度和见解。

谢谢您的支持,期待与您在下一篇文章中再次相遇!

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

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

相关文章

wordpress外贸成品网站模板

首页大图slider轮播&#xff0c;橙色风格的wordpress外贸网站模板 https://www.zhanyes.com/waimao/6250.html 蓝色经典风格的wordpress外贸建站模板 https://www.zhanyes.com/waimao/6263.html

C# Winform .net6自绘的圆形进度条

using System; using System.Drawing; using System.Drawing.Drawing2D; using System.Windows.Forms;namespace Net6_GeneralUiWinFrm {public class CircularProgressBar : Control{private int progress 0;private int borderWidth 20; // 增加的边框宽度public int Progr…

通过VSCode开发Python项目

一、插件准备 Python 插件&#xff0c;必须 autoDocstring 生成注释&#xff0c;和Pycharm一样输入三个引号"""会生产注释结构 Todo Tree 高亮显示 TODO/FIXME 二、python相关设置 一&#xff09;设置python环境 按"F1"打开命令面板&#xff08;…

openai公司的chatgpt-3.5参数库内还未增加sora的语料信息

openai公司的chatgpt-3.5参数库内还未增加sora的语料信息&#xff01;我想通过openai公司的chatgpt3.5来了解一下关于sora的技术信息&#xff0c;结果呢&#xff0c;它竟然回答不知道sora是什么。看来&#xff0c;sora的语料库信息还未来得及加入chatgpt3.5的训练模型中。 如图…

vue的十大面试题详情

1 v-show与v-if区别 v-if与v-show可以根据条件的结果,来决定是否显示指定内容&#xff1a; v-if: 条件不满足时, 元素不会存在. v-show: 条件不满足时, 元素不会显示(但仍然存在). <div id"app"><button click"show !show">点我</but…

ELAdmin 隐藏添加编辑按钮

使用场景 做了一个监控模块&#xff0c;数据都是定时生成的&#xff0c;所以不需要手动添加和编辑功能。 顶部不显示 可以使用 true 或者 false 控制现实隐藏 created() {this.crud.optShow {add: false,edit: false,del: true,download: true,reset: true}},如果没有 crea…

ubuntu服务器部署gitlab docker并配置nginx反向代理https访问

拉取镜像 docker pull gitlab/gitlab-ce运行容器 docker run --detach \--publish 9080:80 --publish 9022:22 --publish 9443:443\--namegitlab \--restartalways \--volume /home/docker/gitlab/config:/etc/gitlab \--volume /home/docker/gitlab/logs:/var/log/gitlab \-…

直接查看电脑几核芯几线程的方法

之前查看电脑几核芯几线程时都是点击 此电脑->属性->设备管理器->处理器 但是这样并不能判断是否有多线程 譬如这里&#xff0c;是2核芯2线程还是4核芯&#xff1f; 实际上&#xff0c;打开任务管理器后点击性能查看核芯线程数即可 所以示例这台电脑是4核芯而不是2…

PDF控件Spire.PDF for .NET【安全】演示:如何在 PDF 中添加签名字段

Spire.PDF for .NET 是一款独立 PDF 控件&#xff0c;用于 .NET 程序中创建、编辑和操作 PDF 文档。使用 Spire.PDF 类库&#xff0c;开发人员可以新建一个 PDF 文档或者对现有的 PDF 文档进行处理&#xff0c;且无需安装 Adobe Acrobat。 E-iceblue 功能类库Spire 系列文档处…

html的表单标签(上):form标签和input标签

表单标签 表单是让用户输入信息的重要途径。 用表单标签来完成与服务器的一次交互&#xff0c;比如你登录QQ账号时的场景。 表单分成两个部分&#xff1a; 表单域&#xff1a;包含表单元素的区域&#xff0c;用form标签来表示。表单控件&#xff1a;输入框&#xff0c;提交按…

微信小程序swiper 视频中间大,两边小,轮播滑到中间视频自动播放组件教程

静态效果&#xff1a; 进入下面小程序可以体验效果&#xff0c;点击底部 看剧 栏目 一、创建小程序组件 二、代码 1、WXML <view class"swiper-wrapper" style"background-image:url(/asset/image/hot-banner.jpg);background-size: 100% 100%;">…

android 15

https://android-developers.googleblog.com/2024/02/first-developer-preview-android15.html android 15的预览版出了&#xff0c;这个版本的发布计划大概是这样的&#xff08;大约是今年8月发布最终版本&#xff09; https://developer.android.com/about/versions/15/over…

Java+Swing+Txt实现通讯录管理系统

目录 一、系统介绍 1.开发环境 2.技术选型 3.功能模块 4.系统功能 1.系统登录 2.查看联系人 3.新增联系人 4.修改联系人 5.删除联系人 5.工程结构 二、系统展示 1.登录页面 2.主页面 3.查看联系人 4.新增联系人 5.修改联系人 三、部分代码 Login FileUtils …

BUGKU-WEB 源代码

题目描述 题目截图如下&#xff1a; 进入场景看看&#xff1a; 解题思路 你说啥就是啥&#xff1a;去源代码吧 相关工具 URL解码平台&#xff1a;https://www.iamwawa.cn/urldecode.html 解题步骤 随便输入试试 2. 看看源码 存在script&#xff1a; <script> …

芯课堂 | 一种无霍尔直流无刷电机控制器

​本次介绍一种无霍尔直流无刷电机控制器&#xff0c;该电机控制器包括&#xff1a;MCU控制模块的输出端连接预驱动模块的输入端&#xff0c;预驱动模块的输出端连接驱动模块的输入端&#xff0c;驱动模块的输出端连接无霍尔电机的三相输入端&#xff1b;驱动模块包括由三路集成…

【前端】Vue-Cli 快速创建Vue3项目及一些项目初始化相关

文章目录 前言1. 安装1.1 安装 Vue 脚手架1.2 创建项目1.3 本地运行项目 2. 推送到仓库2.1 远程仓库准备2.2 关于gitIgnore文件2.3 通过git推送至远程仓库 3. 补充与总结3.1 npm 版本是否要升级到最新&#xff1f;3.2 这个项目建议的各种版本3.3 一般前端gitIgnore文件3.4 推荐…

二.重新回炉Spring Framework:Spring Framework主要组件概览

1.写在前面的话 这里主要简单说一下Spring Framework的几个核心组件的总体情况。为了比较直观&#xff0c;这里使用了ClassPathXmlApplicationContext的类图来进行说明。它基本上包含了 IoC 体系中大部分的核心类和接口。类图如下图所示&#xff1a; 2.Resource 组件体系 R…

【C语言】长篇详解,字符系列篇2-----受长度限制的字符串函数,strncpy,strncat,strncmp函数的使用和模拟实现【图文详解】

欢迎来CILMY23的博客喔&#xff0c;本期系列为【【C语言】长篇详解&#xff0c;字符系列篇2-----“混杂”的字符串函数&#xff0c;字符串函数的使用和模拟实现【图文详解】&#xff0c;图文讲解各种字符串函数&#xff0c;带大家更深刻理解C语言中各种字符串函数的应用&#x…

【常识】大数据设计基础知识

底层存储&#xff1a;hadoop&#xff08;hdfsmapreduce&#xff09; Hadoop已经有十几年的历史&#xff0c;它是大数据领域的存储基石&#xff0c;HDFS目前仍然没有成熟替代品;MapR 文件系统在业内已经具有一定知名度了&#xff0c;不仅 MapR 宣布它自己的文件系统比 HDFS 快2-…

Python 文本处理和语义分析2 使用m3e对文本向量化

说明 向量化将会是下一阶段演进的目标。 在过去的实践中&#xff0c;向量或者矩阵其实是最贴近工具端的。 以sklearn为例&#xff0c;虽然原始数据可能还是自然语言&#xff0c;但是在最终执行 fit或者predict之前&#xff0c;数据一般都转为了矩阵形态(numpy)。也就是说&…