【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.hqwc.cn/news/196050.html

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

相关文章

3-docker安装centos7

CentOS7.9下安装完成docker后&#xff0c;后续我们可以在其上安装centos7系统。具体操作如下&#xff1a; 1.以root用户登录CentOS7.9服务器&#xff0c;拉取centos7 images 命令&#xff1a; docker pull centos:centos7 2.加载centos7 images并登录验证 命令&#xff1a;…

一种用于脑肿瘤和组织分割的具有体积特征对齐的三维跨模态特征交互网络

A 3D Cross-Modality Feature Interaction Network With Volumetric Feature Alignment for Brain Tumor and Tissue Segmentation 一种用于脑肿瘤和组织分割的具有体积特征对齐的三维跨模态特征交互网络背景贡献实验方法Cross-Modality Feature Interaction ModuleVolumetric …

【面试经典150 | 算术平方根】

文章目录 写在前面Tag题目来源解题思路方法一&#xff1a;数学表达式方法二&#xff1a;二分法 其他语言python3 写在最后 写在前面 本专栏专注于分析与讲解【面试经典150】算法&#xff0c;两到三天更新一篇文章&#xff0c;欢迎催更…… 专栏内容以分析题目为主&#xff0c;并…

树,二叉树,二叉树遍历,哈夫曼树(详解+刷题)

&#x1f442; 后街男孩经典之精选 - 歌单 - 网易云音乐 &#x1f442; 年轮&#xff08;电视剧《花千骨》男声版插曲&#xff09; - 汪苏泷 - 单曲 - 网易云音乐 目录 &#x1f33c;5.1 -- 树 &#x1f33c;5.2 -- 二叉树 1&#xff0c;性质 2&#xff0c;存储 3&#x…

Figma 插件学习(一)

一.插件介绍 插件在文件中运行&#xff0c;执行一个或多个用户操作&#xff0c;并允许用户自定义其体验或创建更高效的工作流程。 插件通过专用插件API与Figma的编辑器交互。还可以利用外部Web API。 1.插件API 插件API支持读写功能&#xff0c;允许查看、创建和修改文件的…

开源WIFI继电器之硬件电路

一、原理图 源文件 二、原理图说明 1、器件说明 U4&#xff1a;ESP8285模块 U6&#xff1a;触发器 U3&#xff1a;继电器 2、继电器状态检测说明 检测继电器线圈是否通电来判断继电器是否导通&#xff0c;当Q1不导通时&#xff0c;Q1集电极的电压为3.3V&#xff0c;经…

【项目管理】中途接手的项目应对实用指南

导读&#xff1a;作为项目经理中途接手项目往往不可避免&#xff0c;为了保证项目成功需要项目经理额外考虑更多的因素和处理相关问题&#xff0c;也往往带来很大的挑战性。本文提供可应对借鉴的思路&#xff0c;在一定程度上可以作为最佳实践。 目录 1、首先、了解项目项目背…

[AutoSar]导出task mapping 表到excel

目录 关键词平台说明背景实现方法 关键词 嵌入式、C语言、autosar 平台说明 项目ValueOSautosar OSautosar厂商vector芯片厂商TI编程语言C&#xff0c;C编译器HighTec (GCC) 背景 为了做文档输出&#xff0c;要导出task mapping 到excel。 实现方法 1.按住shift&#xf…

Windows核心编程 静态库与动态库

资源文件 .rc 文件 会被 rc.exe 变成 .res 文件(二进制文件) 在链接时链接进入 .exe 文件 一、如何保护源码 程序编译链接过程 不想让别人拿到源代码&#xff0c;但是想让其使用功能&#xff0c;根据上图观察&#xff0c;把自己生成的obj给对方&#xff0c;对方拿到obj后&…

ps找不到msvcp140.dll怎么办?亲测5个有效的修复方法分享

运行Photoshop时提示找不到MSVCP140.dll&#xff0c;这是因为计算机MSVCP140.dll文件丢失或者损坏。msvcp140.dll是微软Visual C 2015运行库的一部分&#xff0c;它包含了许多用于支持C运行的函数和类。当我们在使用某些程序时&#xff0c;如果这个程序依赖于msvcp140.dll&…

Day35力扣打卡

打卡记录 相邻字符不同的最长路径&#xff08;树状DP&#xff09; 链接 若节点也存在父节点的情况下&#xff0c;传入父节点参数&#xff0c;若是遍历到父节点&#xff0c;直接循环里 continue。 class Solution:def longestPath(self, parent: List[int], s: str) -> in…

一个C语言程序的分析:运行速度和文件大小以及变量初始值

环境 Ubuntu 22.04gcc 11.4.0Window 11Microsoft Visual Studio Community 2022 (64-bit) - Current Version 17.6.2 运行速度 一个C程序 test1.c 如下&#xff1a; int array[30000][30000];int main() {for (int i 0; i < 30000; i)for (int j 0; j < 30000; j) …