【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.mzph.cn/news/148658.shtml

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

相关文章

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

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

OpenGL_Learn13(材质)

1. 材质 cube.vs #version 330 core layout (location 0) in vec3 aPos; layout (location 0 ) in vec3 aNormal;out vec3 FragPos; out vec3 Normal;uniform mat4 model; uniform mat4 view; uniform mat4 projection;void main() {FragPosvec3(model*vec4(aPos,1.0));Norma…

从零开始:Rust环境搭建指南

大家好&#xff01;我是lincyang。 今天&#xff0c;我们将一起探讨如何从零开始搭建Rust开发环境。 Rust环境搭建概览 Rust是一种系统编程语言&#xff0c;以其安全性、并发性和性能闻名。搭建Rust环境是学习和使用这一语言的第一步。 第一步&#xff1a;安装Rust Rust的…

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;…

Activiti7工作流

文章目录 一、工作流介绍1.1 概念1.2 适用行业1.3 应用领域1.4 传统实现方式1.5 什么是工作流引擎 二、什么是Activiti7&#xff1f;2.1 概述2.2 Activiti7内部核心机制2.3 BPMN2.4 Activiti如何使用2.4.1 整合Activiti2.4.2 业务流程建模2.4.3 部署业务流程2.4.4 启动流程实例…

WPF中行为与触发器的概念及用法

完全来源于十月的寒流&#xff0c;感谢大佬讲解 一、行为 (Behaviors) behaviors的简单测试 <Window x:Class"Test_05.MainWindow"xmlns"http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x"http://schemas.microsoft.com/winf…

DevToys:开发者的多功能瑞士军刀,让编程更高效!

DevToys&#xff1a;开发者的多功能瑞士军刀&#xff0c;让编程更高效&#xff01; DevToys 是一款专为开发者设计的实用工具&#xff0c;它能够帮助用户完成日常的开发任务&#xff0c;如格式化 JSON、比较文本和测试正则表达式&#xff08;RegExp&#xff09;。它的优势在于…

Selenium UI 自动化

一、Selenium 自动化 1、什么是Selenium&#xff1f; Selenium是web应用中基于UI的自动化测试框架。 2、Selenium的特点&#xff1f; 支持多平台、多浏览器、多语言。 3、自动化工作原理&#xff1f; 通过上图&#xff0c;我们可以注意到3个角色&#xff0c;下面具体讲解一…

VBA之Word应用:文档(Document)的书签

《VBA之Word应用》&#xff08;版权10178982&#xff09;&#xff0c;是我推出第八套教程&#xff0c;教程是专门讲解VBA在Word中的应用&#xff0c;围绕“面向对象编程”讲解&#xff0c;首先让大家认识Word中VBA的对象&#xff0c;以及对象的属性、方法&#xff0c;然后通过实…

数据结构之链表练习与习题详细解析

个人主页&#xff1a;点我进入主页 专栏分类&#xff1a;C语言初阶 C语言程序设计————KTV C语言小游戏 C语言进阶 C语言刷题 数据结构初阶 欢迎大家点赞&#xff0c;评论&#xff0c;收藏。 一起努力&#xff0c;一起奔赴大厂。 目录 1.前言 2.习题解…

CICD 持续集成与持续交付——git

git使用 [rootcicd1 ~]# yum install -y git[rootcicd1 ~]# mkdir demo[rootcicd1 ~]# cd demo/ 初始化版本库 [rootcicd1 demo]# git init 查看状态 [rootcicd1 demo]# git status[rootcicd1 demo]# git status -s #简化输出 [rootcicd1 demo]# echo test > README.md[roo…

python自动化标注工具+自定义目标P图替换+深度学习大模型(代码+教程+告别手动标注)

省流建议 本文针对以下需求&#xff1a; 想自动化标注一些目标不再想使用yolo想在目标检测/语意分割有所建树计算机视觉项目想玩一玩大模型了解自动化工具了解最前沿模型自定义目标P图替换… 确定好需求&#xff0c;那么我们发车&#xff01; 实现功能与结果 该模型将首先…

6 Redis的慢查询配置原理

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

基于PHP+MySql的酒店信息管理系统的设计与实现

一、系统开发环境 运行环境&#xff1a;phpstudy或者wampserver&#xff0c; 开发工具&#xff1a;vscodephpstorm 数据库&#xff1a;mysql 二、酒店管理系统功能 1.前台功能&#xff1a; 首页客房推荐&#xff0c;周边特色介绍 酒店在线预订 订单查询&#xff0c;可以…

C++各种字符转换

C各种字符转换 一.如何将char数组转化为string类型二. string转char数组&#xff1a;参考 一.如何将char数组转化为string类型 在C中&#xff0c;可以使用string的构造函数或者赋值操作符来将char数组转换为string类型。 方法1&#xff1a;使用string的构造函数 const char* c…

【Web】Ctfshow SSTI刷题记录1

目录 ①web361 362-无过滤 ②web363-过滤单双引号 ③web364-过滤单双引号和args ④web365-过滤中括号[]、单双引号、args ⑤web366-过滤单双引号、args、中括号[]、下划线 ⑦web367-过滤单双引号、args、中括号[]、下划线、os ⑧web368-过滤单双引号、args、中括号[]、下…

原理Redis-动态字符串SDS

动态字符串SDS Redis中保存的Key是字符串&#xff0c;value往往是字符串或者字符串的集合。可见字符串是Redis中最常用的一种数据结构。 不过Redis没有直接使用C语言中的字符串&#xff0c;因为C语言字符串存在很多问题&#xff1a; 获取字符串长度的需要通过运算非二进制安全…

qt-C++笔记之两个窗口ui的交互

qt-C笔记之两个窗口ui的交互 code review! 文章目录 qt-C笔记之两个窗口ui的交互0.运行1.文件结构2.先创建widget项目&#xff0c;搞一个窗口ui出来3.项目添加第二个widget窗口出来4.补充代码4.1.qt_widget_interaction.pro4.2.main.cpp4.3.widget.h4.4.widget.cpp4.5.second…

ClickHouse数据一致性

查询CK手册发现&#xff0c;即便对数据一致性支持最好的Mergetree&#xff0c;也只是保证最终一致性&#xff1a; 我们在使用 ReplacingMergeTree、SummingMergeTree 这类表引擎的时候&#xff0c;会出现短暂数据不一致的情况。 在某些对一致性非常敏感的场景&#xff0c;通常有…

庖丁解牛:NIO核心概念与机制详解 03 _ 缓冲区分配、包装和分片

文章目录 Pre概述缓冲区分配和包装 &#xff08;allocate 、 wrap&#xff09;缓冲区分片 (slice)缓冲区份片和数据共享只读缓冲区 &#xff08;asReadOnlyBuffer&#xff09;直接和间接缓冲区 (allocateDirect)内存映射文件 I/O将文件映射到内存(map) Pre 庖丁解牛&#xff1…