【c++随笔13】多态

【c++随笔13】多态

  • 多态性(Polymorphism)在面向对象编程中是一个重要概念,它允许以统一的方式处理不同类型的对象,并在运行时动态确定实际执行的方法或函数。
  • 一、什么是多态性?
    • 1、关键概念:C++的多态性
    • 2、多态定义
    • 3、没有 静态多态、动态多态
  • 二、多态的详细介绍
    • 1、多态的构成条件
    • 2、覆盖(override)——重写
    • 3、多态构成的两个意外
      • 3.1、协变——构成多态
      • 3.2、父虚子非虚——构成多态
    • 4、析构函数的重写
      • 1. 直接定义对象
      • 2. 使用new操作符在堆上创建对象
      • 3、结论:在堆上构建对象,且基类指针指向派生类的情况下,如果不加virtual,会发生内存泄漏,派生类不会析构。
    • 5、final (C++11)
    • 6、override(C++11)
    • 7、重载、覆盖、隐藏的对比
  • 三、抽象类
    • 1、纯虚函数
    • 2、 抽象类(abstract class)
    • 3、抽象类指针
    • 4、- 抽象类实例化?
    • 5、接口继承(Interface Inheritance)和实现继承(Implementation Inheritance)是面向对象编程中的两种继承方式。

原创作者:郑同学的笔记
原创地址:https://zhengjunxue.blog.csdn.net/article/details/131858812
qq技术交流群:921273910

多态性(Polymorphism)在面向对象编程中是一个重要概念,它允许以统一的方式处理不同类型的对象,并在运行时动态确定实际执行的方法或函数。

一、什么是多态性?

1、关键概念:C++的多态性

我们查看《C++ Primer 第5版》第15.3章节 虚函数中的介绍(p537页)

OOP的核心思想是多态性(polymorphism)。多态性这个词源自希腊语,其含义是“多种形式”。我们把具有继承关系的多个类型称为多态类型,因为我们能使用这些类型的“多种形式”而无须在意它们的差异。引用或指针的静态类型与动态类型不同这一事实正是C++语言支持多态性的根本所在。


当我们使用基类的引用或指针调用基类中定义的一个函数时,我们并不知道该函数真正作用的对象是什么类型,因为它可能是一个基类的对象也可能是一个派生类的对象。如果该函数是虚函数,则直到运行时才会决定到底执行哪个版本,判断的依据是引用或指针所绑定的对象的真实类型。


另一方面,对非虚函数的调用在编译时进行绑定。类似的,通过对象进行的函数(虚函数或非虚函数)调用也在编译时绑定。对象的类型是确定不变的,我们无论如何都不可能令对象的动态类型与静态类型不一致。因此,通过对象进行的函数调用将在编译时绑定到该对象所属类中的函数版本上。


Note当且仅当对通过指针或引用调用虚函数时,才会在运行时解析该调用,也只有在这种情况下对象的动态类型才有可能与静态类型不同。

2、多态定义

我们依然查看《C++ Primer 第5版》第15章节末尾 术语表中的介绍(p576页)

多态性(polymorphism)当用于面向对象编程的范畴时,多态性的含义是指程序能通过引用或指针的动态类型获取类型特定行为的能力。

动态类型(dynamic type)对象在运行时的类型。引用所引对象或者指针所指对象的动态类型可能与该引用或指针的静态类型不同。基类的指针或引用可以指向一个派生类对象。在这样的情况中,静态类型是基类的引用(或指针),而动态类型是派生类的引用(或指针)。

静态类型(static type)对象被定义的类型或表达式产生的类型。静态类型在编译时是已知的。

3、没有 静态多态、动态多态

我们看网上有很多资料介绍动态时,都会提到多态分为静态多态(比如函数重载等)和动态多态,而当我们看了上面书中的定义和介绍后会明白,网上的说法是有问题的。

在c++领域:

  • 只有多态、不区分静态多态和动态多态;
  • 网上说的c++动态多态就是指的c++中的多态;
  • 网上说的静态多态,不符合《C++ Primer 第5版》多态的概念;
  • 静态多态按照《C++ Primer 第5版》中书写demo,无法实现多态;

二、多态的详细介绍

动态多态性是在运行时确定方法或函数的调用,根据实际对象的类型进行动态绑定。这种多态性通过虚函数和基类指针或引用来实现。

简单来说,
多态: 就是多种形态,不同的对象去完成同样的事情会产生不同的结果。
举个例子:就拿购票系统来说,不同的人对于购票这个行为产生的结果就是不同的,学生购票时购买的是半价票,普通人购票的时候购买的是全价票。

1、多态的构成条件

继承中想要构成多态,必须满足以下两个条件:

① 必须是子类的虚函数重写成父类函数(重写:三同 + 虚函数)
② 必须是父类的指针或者引用去调用虚函数。

  • 三同指的是:同函数名、同参数、同返回值。
  • 虚函数:即被 virtual 修饰的类成员函数。
  • 指针调用
#include <iostream>
using namespace std;class Person {
public:Person(const char* name): _name(name) {}// 虚函数virtual void BuyTicket() {cout << _name << ": " << "Person-> 买票   全价 100¥" << endl;}protected:string _name;
};class Student : public Person {
public:Student(const char* name): Person(name) {}// 虚函数 + 函数名/参数/返回 -> 重写(覆盖)virtual void BuyTicket() {cout << _name << ": " << "Student-> 买票 半价 50¥" << endl;}
};class Soldier : public Person {
public:Soldier(const char* name): Person(name) {}// 虚函数 + 函数名/参数/返回 -> 重写(覆盖)virtual void BuyTicket() {cout << _name << ": " << "Soldier-> 优先买预留票 全价 100¥" << endl;}
};/* 接收身份 */
void Pay(Person* ptr) {ptr->BuyTicket();  // 到底是谁在买票,取决于传来的是谁delete ptr;
}int main()
{Person* p1 = new Person("小明爸爸");Student* stu = new Student("小明");Soldier* so = new Soldier("小明爷爷");Pay(p1);Pay(stu);Pay(so);return 0;
}

输出

在这里插入图片描述

  • 引用调用
/* 接收身份 */
void Pay(Person& ptr) {ptr.BuyTicket();  // 到底是谁在买票,取决于传来的是谁
}int main()
{Person p1("小明爸爸");Student stu("小明");Soldier so("小明爷爷");Pay(p1);Pay(stu);Pay(so);return 0;
}

2、覆盖(override)——重写

我们依然查看《C++ Primer 第5版》第15章节末尾 术语表中的介绍(p576页)

  • 覆盖(override)派生类中定义的虚函数如果与基类中定义的同名虚函数有相同的形参列表,则派生类版本将覆盖基类的版本。

覆盖也被有的文章叫做”重写“。用 virtual 虚函数,并且做到函数名、参数和返回值相同,就能够达到 “重写” 的效果:

重写是为了将一个已有的事物进行某些改变以适应新的要求。
重写是子类对父类的允许访问的方法的实现过程进行重新编写,返回值和形参都不能改变。 即:“外壳不变,核心重写。”

3、多态构成的两个意外

刚才说了,三同+虚函数,就能达到重写的效果(也就是多态)。但是,还有两个意外,也能达成多态的效果。

3.1、协变——构成多态

  • C++中的协变(Covariance)指的是派生类可以返回基类中相同函数签名的返回类型的子类型。

  • 在C++中,当一个虚函数在基类中使用了virtual关键字声明为虚函数时,派生类可以对该虚函数进行重写,并且在派生类中返回类型可以是基类返回类型的子类型。这种返回类型的子类型关系称为协变。

协变的类型必须是父子关系。

观察下面的代码,并没有达到 “三同” 的标准,它的返回值是不同的,但依旧构成多态:

class A {};
class B : public A {};class Person {
public:virtual A* f() {cout << "virtual A* Person::f()" << endl;return nullptr;}
};class Student : public Person {
public:virtual B* f() {cout << "virtual B* Student:::f()" << endl;return nullptr;};
};int main(void)
{Person p;Student s;Person* ptr = &p;ptr->f();ptr = &s;ptr->f();return 0;
}

输出

在这里插入图片描述

当class A、class B是父子关系时,就不能协变:

3.2、父虚子非虚——构成多态

现在来讲第二个例外。

  • 父类的虚函数没了无法构成多态:
  • 但是,子类的虚函数没了却能构成多态:
#include <iostream>
using namespace std;class A {};
class B : public A {};class Person {
public:virtual A* f() {cout << "virtual A* Person::f()" << endl;return nullptr;}
};class Student : public Person {
public:B* f() {cout << "virtual B* Student:::f()" << endl;return nullptr;};
};int main(void)
{Person p;Student s;Person* ptr = &p;ptr->f();ptr = &s;ptr->f();return 0;
}

输出

在这里插入图片描述

4、析构函数的重写

1. 直接定义对象

#include <iostream>
using namespace std;class Person {
public:~Person() {  //不加virtual// virtual ~Person() { //加virtualcout << "~Person()" << endl;}
};class Student : public Person {
public:~Student() {cout << "~Student()" << endl;}
};int main(void)
{Person p;Student s;return 0;
}
  • 加virtual输出
    在这里插入图片描述

  • 不加virtual输出
    在这里插入图片描述

2. 使用new操作符在堆上创建对象

#include <iostream>
using namespace std;class Person {
public:~Person() {  //不加virtual//virtual ~Person() { //加virtualcout << "~Person()" << endl;}
};class Student : public Person {
public:~Student() {cout << "~Student()" << endl;}
};int main(void)
{cout << "=================不加virtual======================\n";Person *ptr = new Person();delete ptr;cout << "=======================================\n";Student *ptr2 = new Student();delete ptr2;cout << "=======================================\n";Person *ptr3 = new Student();delete ptr3;return 0;
}
  • 不加virtual
    在这里插入图片描述

  • 加virtual
    在这里插入图片描述

刚才我们看到了,如果这里不加 virtual,~Student 是没有调用析构的。
这其实是非常致命的,是不经意间会发生的内存泄露。

3、结论:在堆上构建对象,且基类指针指向派生类的情况下,如果不加virtual,会发生内存泄漏,派生类不会析构。

5、final (C++11)

在C++中,final是一个关键字,用于修饰类、函数或虚函数,具有不同的作用。

  1. 修饰类:使用final关键字修饰类时,表示该类是最终类,不能被其他类继承。例如:
class Base final {// ...
};class Derived : public Base {  // 错误,Derived不能继承自final类Base// ...
};

在上述示例中,Base类被声明为final,因此Derived类不能继承自Base类。

  1. 修饰函数:使用final关键字修饰成员函数时,表示该函数是最终版本,不能被派生类重写。例如:
class Base {
public:virtual void func() final {// ...}
};class Derived : public Base {
public:void func() override {  // 错误,无法重写被声明为final的函数// ...}
};

在上述示例中,Base类中的func()函数被声明为final,因此Derived类无法对其进行重写。

  1. 修饰虚函数:与修饰普通成员函数类似,使用final关键字修饰虚函数时,表示该虚函数是最终版本,不能被派生类重写。例如:
class Base {
public:virtual void func() final {// ...}
};class Derived : public Base {
public:void func() override {  // 错误,无法重写被声明为final的虚函数// ...}
};

在上述示例中,Base类中的虚函数func()被声明为final,因此Derived类无法对其进行重写。

通过使用final关键字,可以显式地阻止类、函数或虚函数被继承、重写或覆盖,从而提高程序的安全性和可靠性。

6、override(C++11)

override是C++11引入的关键字,用于显式地标记派生类中对基类虚函数的重写。它的主要作用是增加代码的可读性和可维护性,并提供编译器的静态检查,避免错误的重写行为。

在C++中,当派生类要重写基类的虚函数时,可以使用override关键字进行标记。通过使用override关键字,可以确保派生类的函数签名与基类的虚函数完全匹配,否则编译器会发出错误。这有助于及时发现错误的重写行为。

以下是使用override关键字的示例:

class Base {
public:virtual void func() const {// ...}
};class Derived : public Base {
public:void func() const override {// ...}
};

在上述示例中,Base类中的虚函数func()被定义为virtual void func() const,而在Derived类中,重写的函数也被定义为void func() const,并使用override关键字进行标记。如果Derived类的函数签名与基类的虚函数不匹配,或者没有正确使用override关键字,编译器将会报错。

7、重载、覆盖、隐藏的对比

在这里插入图片描述

三、抽象类

1、纯虚函数

我们依然查看《C++ Primer 第5版》第15章节末尾 术语表中的介绍(p576页)

  • 纯虚函数(pure virtual)在类的内部声明虚函数时,在分号之前使用了=0。一个纯虚函数不需要(但是可以)被定义。含有纯虚函数的类是抽象基类。如果派生类没有对继承而来的纯虚函数定义自己的版本,则该派生类也是抽象的。

纯虚函数是通过在函数声明后面加上= 0来声明的,表示该函数没有实现,派生类必须重写它。

virtual void pureVirtualFunction() = 0;

在上述示例中,纯虚函数pureVirtualFunction()。

  • 纯虚函数是否可以实现?
    纯虚函数也是可以实现的:
/* 抽象类 */
class Car {
public:// 实现没有价值,因为压根没有对象会调用它virtual void Drive() = 0 {      // 纯虚函数cout << "Drive()" << endl;   }
};

2、 抽象类(abstract class)

  • 包含纯虚函数的类,就是 抽象类(abstract class),也叫接口类。
class AbstractClass {
public:virtual void pureVirtualFunction() = 0;
};

在上述示例中,AbstractClass是一个抽象类,它具有一个纯虚函数pureVirtualFunction()。派生类必须重写这个函数。

抽象类可以包含纯虚函数(没有实现)和带有实现的函数

3、抽象类指针

虽然父类是抽象类不能定义对象,但是可以定义指针。

定义指针时如果 new 父类对象因为是纯虚函数,自然是 new 不出来的,但是可以 new 子类对象:

#include <iostream>
using namespace std;/* 抽象类 */
class Car {
public:virtual void Drive() = 0;
};class Benz : public Car {
public:virtual void Drive() {cout << "Benz-舒适" << endl;}
};int main(void)
{Car* pBenz1 = new Benz;pBenz1->Drive();Benz* pBenz2 = new Benz;pBenz2->Drive();return 0;
}

4、- 抽象类实例化?

抽象类不能实例化出对象,子类即使在继承后也不能实例化出对象,除非子类重写。

5、接口继承(Interface Inheritance)和实现继承(Implementation Inheritance)是面向对象编程中的两种继承方式。

接口继承指的是一个类从一个或多个接口中继承方法声明,但并不继承这些方法的具体实现。接口只包含纯虚函数(在C++中使用纯虚函数定义接口)或者抽象方法(在其他语言中)。通过接口继承,一个类可以实现多个接口,从而表达出它具备了多个行为或功能。

实现继承指的是子类从父类中继承方法声明和实现。实现继承建立了类的层次结构,允许子类继承并重用父类的代码。子类可以通过继承父类的属性和方法,并且可以根据需要添加新的属性和方法,甚至可以重写父类的方法来改变其行为。

  • 普通函数的继承是一种实现继承,子类继承了父类函数,可以使用函数,继承的是函数的实现。
  • 虚函数的继承是一种接口继承,子类继承的是父类虚函数的接口,目的是为了重写,
  • 达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。
  • 出现虚函数就是为了提醒你重写的,以实现多态。如果虚函数不重写,那写成虚函数就没价值了。

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

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

相关文章

算法——动态规划(新)

什么是动态规划&#xff1f; 动态规划算法的基本思想-求解步骤-基本要素和一些经典的动态规划问题【干货】-CSDN博客 一、三步问题 面试题 08.01. 三步问题 - 力扣&#xff08;LeetCode&#xff09; 思路 我们要知道&#xff0c;走楼梯&#xff0c;前三个阶梯步数已经知道&…

6 Redis的慢查询配置原理

1、redis的命令执行流程 redis的慢查询只针对步骤3 默认情况下&#xff0c;慢查询的阈值是10ms

Ps:变换

可以向选区、整个图层、多个图层或图层蒙版应用变换 Transform&#xff0c;还可以向路径、矢量形状、矢量蒙版、选区边界或 Alpha 通道应用变换。 若要变换栅格&#xff08;像素&#xff09;图像&#xff0c;建议先将其转换为智能对象&#xff0c;以便进行非破坏性的变换。 Ps菜…

【Django-DRF用法】多年积累md笔记,第(4)篇:Django-DRF反序列化详解

本文从分析现在流行的前后端分离Web应用模式说起&#xff0c;然后介绍如何设计REST API&#xff0c;通过使用Django来实现一个REST API为例&#xff0c;明确后端开发REST API要做的最核心工作&#xff0c;然后介绍Django REST framework能帮助我们简化开发REST API的工作。 全…

[内存泄漏][PyTorch](create_graph=True)

PyTorch保存计算图导致内存泄漏 1. 内存泄漏定义2. 问题发现背景3. github中pytorch源码关于这个问题的讨论 1. 内存泄漏定义 内存泄漏&#xff08;Memory Leak&#xff09;是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放&#xff0c;造成系统内存的浪费&#…

MIB 6.1810实验Xv6 and Unix utilities(3)pingpong

Mit6.S081-实验1-Xv6 and Unix utilities-pingpong问题_Isana_Yashiro的博客-CSDN博客 Write a user-level program that uses xv6 system calls to ping-pong a byte between two processes over a pair of pipes, one for each direction. The parent should send a byte to…

【计算思维】蓝桥杯STEMA 科技素养考试真题及解析 4

1、下列哪个选项填到填到下图空缺处最合适 A、 B、 C、 D、 答案&#xff1a;D 2、按照如下图的规律摆放正方形&#xff0c;第 5 堆正方形的个数是 A、13 B、14 C、15 D、16 答案&#xff1a;D 3、从右面观察下面的立体图形&#xff0c;看到的是 A、 B、 C、 D、 答…

hyperledger fabric2.4测试网络添加组织数量

!!!修改内容比较繁琐,预期未来提供模板修改 修改初始配置文件,初始添加3个组织 organizations文件夹 /cryptogen文件夹下创建文件crypto-config-org3.yaml,内容如下: PeerOrgs:# ---------------------------------------------------------------------------# Org3# ----…

STM32电源名词解析

先来简单了解一下各种电源端口的命名 VCC&#xff1a;Ccircuit 表示电路的意思, 即接入电路的电压 VDD&#xff1a;Ddevice 表示器件的意思, 即器件内部的工作电压。 VSS&#xff1a;Sseries 表示公共连接的意思&#xff0c;通常指电路公共接地端电压。 GND&#xff1a;在电…

SpringCloud微服务注册中心:Nacos介绍,微服务注册,Ribbon通信,Ribbon负载均衡,Nacos配置管理详细介绍

微服务注册中心 注册中心可以说是微服务架构中的”通讯录“&#xff0c;它记录了服务和服务地址的映射关系。在分布式架构中&#xff0c;服务会注册到这里&#xff0c;当服务需要调用其它服务时&#xff0c;就这里找到服务的地址&#xff0c;进行调用。 微服务注册中心 服务注…

wpf devexpress自定义编辑器

打开前一个例子 步骤1-自定义FirstName和LastName编辑器字段 如果运行程序&#xff0c;会通知编辑器是空。对于例子&#xff0c;这两个未命名编辑器在第一个LayoutItem(Name)。和最终用户有一个访客左右编辑器查阅到First Name和Last Name字段&#xff0c;分别。如果你看到Go…

第20章 数据库编程

通过本章需要理解JDBC的核心设计思想以及4种数据库访问机制&#xff0c;理解数据库连接处理流程&#xff0c;并且可以使用JDBC进行Oracle数据库的连接&#xff0c;理解工厂设计模式在JDBC中的应用&#xff0c;清楚地理解DriverManager类的作用&#xff0c;掌握Connection、Prep…

【SpringBoot3+Vue3】四【实战篇】-前端(vue基础)

目录 一、项目前置知识 二、使用vscode创建 三、vue介绍 四、局部使用vue 1、快速入门 1.1 需求 1.2 准备工作 1.3 操作 1.3.1 创建html 1.3.2 创建初始html代码 1.3.3 参照官网import vue 1.3.4 创建vue应用实例 1.3.5 准备div 1.3.6 准备用户数据 1.3.7 通过…

【Linux网络】详解使用http和ftp搭建yum仓库,以及yum网络源优化

目录 一、回顾yum的原理 1.1yum简介 yum安装的底层原理&#xff1a; yum的好处&#xff1a; 二、学习yum的配置文件及命令 1、yum的配置文件 2、yum的相关命令详解 3、yum的命令相关案例 三、搭建yum仓库的方式 1、本地yum仓库建立 2、通过http搭建内网的yum仓库 3、…

RabbitMQ消息的可靠性

RabbitMQ消息的可靠性 一 生产者的可靠性 生产者重试 有时候由于网络问题&#xff0c;会出现连接MQ失败的情况&#xff0c;可以配置重连机制 注意&#xff1a;SpringAMQP的重试机制是阻塞式的&#xff0c;重试等待的时候&#xff0c;当前线程会等待。 spring:rabbitmq:conne…

Java读写Jar

Java提供了读写jar的类库Java.util.jar&#xff0c;Java获取解析jar包的工具类如下&#xff1a; import java.io.File; import java.io.IOException; import java.net.URL; import java.net.URLClassLoader; import java.util.Enumeration; import java.util.HashMap; import …

你知道什么是SaaS吗?

你知道什么是SaaS吗&#xff1f; 云服务架构的三个概念 PaaS 英文就是 Platform-as-a-Service&#xff08;平台即服务&#xff09;PaaS&#xff0c;某些时候也叫做中间件。就是把客户采用提供的开发语言和工具&#xff08;例如Java&#xff0c;python, .Net等&#xff09;开…

我叫:选择排序【JAVA】

1.我是个啥子&#xff1f;&#xff1f; 选择式排序&#xff1a;属于内部排序法,从欲排序的数据中,按指定的规则选出某一元素&#xff0c;再依规定交换位置后达到排序的目的。 2.我的思想 基本思想:第一次从arr[0]~arr[n-1]中选取最小值&#xff0c;与arr[0]交换&#xff0c;第…

单张图像3D重建:原理与PyTorch实现

近年来&#xff0c;深度学习&#xff08;DL&#xff09;在解决图像分类、目标检测、语义分割等 2D 图像任务方面表现出了出色的能力。DL 也不例外&#xff0c;在将其应用于 3D 图形问题方面也取得了巨大进展。 在这篇文章中&#xff0c;我们将探讨最近将深度学习扩展到单图像 3…

Transformer中WordPiece/BPE等不同编码方式详解以及优缺点

❤️觉得内容不错的话&#xff0c;欢迎点赞收藏加关注&#x1f60a;&#x1f60a;&#x1f60a;&#xff0c;后续会继续输入更多优质内容❤️ &#x1f449;有问题欢迎大家加关注私戳或者评论&#xff08;包括但不限于NLP算法相关&#xff0c;linux学习相关&#xff0c;读研读博…