【C++ 学习 ⑱】- 多态(上)

news2024/11/25 13:26:20

目录

一、多态的概念和虚函数

1.1 - 用基类指针指向派生类对象

1.2 - 虚函数和虚函数的重写

1.3 - 多态构成的条件

1.4 - 多态的应用场景

二、协变和如何析构派生类对象

2.1 - 协变

2.2 - 如何析构派生类对象

三、C++11 的 override 和 final 关键字


 


一、多态的概念和虚函数

1.1 - 用基类指针指向派生类对象

基类指针可以指向派生类对象,但是通过基类指针只能使用基类的成员(包括成员变量和成员函数),不能使用派生类的成员

#include <iostream>
using namespace std;
​
class Person
{
public:
    Person(const char* name = "张三", int age = 18)
        : _name(name), _age(age)
    { }
​
    void Print() const
    {
        cout << _name << "今年" << _age << "岁了。" << endl;
    }
protected:
    string _name;
    int _age;
};
​
class Student : public Person
{
public:
    Student(const char* name = "张三", int age = 18, int id = 0)
        : Person(name, age), _id(id)
    { }
​
    void Print() const
    {
        cout << _name << "今年" << _age << "岁了,学号是" << _id << "。" << endl;
    }
protected:
    int _id; 
};
​
int main()
{
    Person p("李四", 19);
    Person* pp = &p;
    pp->Print();  // 李四今年19岁了。
    
    Student s("王五", 20, 2);
    pp = &s;
    pp->Print();  // 王五今年20岁了。
    return 0;
}

1.2 - 虚函数和虚函数的重写

如果在基类的成员函数前面加上 virtual 关键字,把它声明为虚函数,并且在派生类中对基类的虚函数进行重写(覆盖),那么当基类指针指向派生类对象时,就可以调用派生类中同名的成员函数,通过派生类中同名的成员函数,也就可以访问派生类对象的成员变量

当派生类中有一个跟基类完全相同的虚函数时,即派生类虚函数与基类虚函数的返回值类型、函数名、参数列表完全相同,就称派生类重写(覆盖)了基类的虚函数

#include <iostream>
using namespace std;
​
class Person
{
public:
    Person(const char* name = "张三", int age = 18)
        : _name(name), _age(age)
    { }
    
    // 声明为虚函数
    virtual void Print() const
    {
        cout << _name << "今年" << _age << "岁了。" << endl;
    }
protected:
    string _name;
    int _age;
};
​
class Student : public Person
{
public:
    Student(const char* name = "张三", int age = 18, int id = 0)
        : Person(name, age), _id(id)
    { }
    
    // 注意:在派生类中重写基类的虚函数时,派生类的虚函数可以不加 virtual 关键字,
    // 但是这种写法不是很规范,不建议这样操作。
    virtual void Print() const
    {
        cout << _name << "今年" << _age << "岁了,学号是" << _id << "。" << endl;
    }
protected:
    int _id;
};
​
int main()
{
    Person p("李四", 19);
    Person* pp = &p;
    pp->Print();  // 李四今年19岁了。
    
    Student s("王五", 20, 2);
    pp = &s;
    pp>->Print();  // 王五今年20岁了,学号是2。
    return 0;
}

有了虚函数,基类指针指向基类对象时就使用基类的成员函数,指向派生类对象时就使用派生类的成员函数,基类指针表现出了多种形态,这种现象我们称之为多态(Polymorphism)

1.3 - 多态构成的条件

通过以上的内容,可以总结出构成多态的条件

  1. 必须存在继承关系;

  2. 继承关系中派生类必须对基类的虚函数进行重写。

  3. 必须通过基类的指针或引用调用虚函数。

因为引用在本质上是通过指针的方式实现的,所以,既然借助基类指针可以实现多态,那么借助基类引用也可以实现多态

Person& rp = p;
p.Print();  // 李四今年19岁了。
Person& rs = s;
rs.Print();  // 王五今年20岁了,学号是2。

1.4 - 多态的应用场景

在应用开发中,我们可以在基类的成员函数中实现基本的功能,并且把基类的成员函数设置为虚函数,留给派生类去扩展和优化、实现个性化的功能

当然,要达到以上的目的,不一定要使用虚函数和多态,例如

#include <iostream>
using namespace std;
​
class Hero
{
public:
    void skillQ() { cout << "英雄释放了 Q 技能" << endl; }
    void skillW() { cout << "英雄释放了 E 技能" << endl; }
    void skillE() { cout << "英雄释放了 W 技能" << endl; }
    void skillR() { cout << "英雄释放了 R 技能" << endl; }
protected:
    int HP;  // 体力值、血量
    int MP;  // 魔法值
    int AD;  // 物理伤害
    int AP;  // 法术伤害
};
​
class A : public Hero
{
public:
    void skillQ() { cout << "英雄 A 释放了 Q 技能" << endl; }
    void skillW() { cout << "英雄 A 释放了 E 技能" << endl; }
    void skillE() { cout << "英雄 A 释放了 W 技能" << endl; }
    void skillR() { cout << "英雄 A 释放了 R 技能" << endl; }
};
​
class B : public Hero
{
public:
    void skillQ() { cout << "英雄 B 释放了 Q 技能" << endl; }
    void skillW() { cout << "英雄 B 释放了 E 技能" << endl; }
    void skillE() { cout << "英雄 B 释放了 W 技能" << endl; }
    void skillR() { cout << "英雄 B 释放了 R 技能" << endl; }
};
​
class C : public Hero
{
public:
    void skillQ() { cout << "英雄 C 释放了 Q 技能" << endl; }
    void skillW() { cout << "英雄 C 释放了 E 技能" << endl; }
    void skillE() { cout << "英雄 C 释放了 W 技能" << endl; }
    void skillR() { cout << "英雄 C 释放了 R 技能" << endl; }
};
​
int main()
{
    int option = 0;
    cout << "请选择英雄(1-A;2-B;3-C):";
    cin >> option;
​
    if (option == 1)
    {
        A a;
        a.skillQ();
        a.skillW();
        a.skillE();
        a.skillR();
    }
    else if (option == 2)
    {
        B b;
        b.skillQ();
        b.skillW();
        b.skillE();
        b.skillR();
    }
    else if (option == 3)
    {
        C c;
        c.skillQ();
        c.skillW();
        c.skillE();
        c.skillR();
    }
    return 0;
}

但是使用多态可以让编程更方便,代码更精简

#include <iostream>
using namespace std;
​
class Hero
{
public:
    virtual void skillQ() { cout << "英雄释放了 Q 技能" << endl; }
    virtual void skillW() { cout << "英雄释放了 E 技能" << endl; }
    virtual void skillE() { cout << "英雄释放了 W 技能" << endl; }
    virtual void skillR() { cout << "英雄释放了 R 技能" << endl; }
protected:
    int HP;  // 体力值、血量
    int MP;  // 魔法值
    int AD;  // 物理伤害
    int AP;  // 法术伤害
};
​
class A : public Hero
{
public:
    virtual void skillQ() { cout << "英雄 A 释放了 Q 技能" << endl; }
    virtual void skillW() { cout << "英雄 A 释放了 E 技能" << endl; }
    virtual void skillE() { cout << "英雄 A 释放了 W 技能" << endl; }
    virtual void skillR() { cout << "英雄 A 释放了 R 技能" << endl; }
};
​
class B : public Hero
{
public:
    virtual void skillQ() { cout << "英雄 B 释放了 Q 技能" << endl; }
    virtual void skillW() { cout << "英雄 B 释放了 E 技能" << endl; }
    virtual void skillE() { cout << "英雄 B 释放了 W 技能" << endl; }
    virtual void skillR() { cout << "英雄 B 释放了 R 技能" << endl; }
};
​
class C : public Hero
{
public:
    virtual void skillQ() { cout << "英雄 C 释放了 Q 技能" << endl; }
    virtual void skillW() { cout << "英雄 C 释放了 E 技能" << endl; }
    virtual void skillE() { cout << "英雄 C 释放了 W 技能" << endl; }
    virtual void skillR() { cout << "英雄 C 释放了 R 技能" << endl; }
};
​
int main()
{
    int option = 0;
    cout << "请选择英雄(1-A;2-B;3-C):";
    cin >> option;
​
    Hero* p = nullptr;
    if (option == 1)
        p = new A;
    else if (option == 2)
        p = new B;
    else if (option == 3)
        p = new C;
​
    if (p)
    {
        p->skillQ();
        p->skillW();
        p->skillE();
        p->skillR();
        delete p;
    }
    return 0;
}


二、协变和如何析构派生类对象

2.1 - 协变

协变就是在派生类中重写基类虚函数时,基类虚函数的返回值类型为基类对象的指针或引用,派生类虚函数的返回值类型为派生类对象的指针或引用

协变是虚函数重写的一种例外

class A {};
​
class B : public A {};
​
class Person
{
public:
    virtual A* func() { return new A; }
};
​
class Student : public Person
{
public:
    virtual B* func() { return new B; }
};

2.2 - 如何析构派生类对象

用基类指针指向派生类对象是多态的精髓,但是如果用基类指针销毁派生类对象的时候,不能调用派生类的析构函数,就可能会造成内存泄漏,因为在应用开发中,我们一般会把释放资源的代码写在析构函数中,例如释放堆区申请的内存空间

#include <iostream>
using namespace std;
​
class Person
{
public:
    ~Person() { cout << "~Person()" << endl; }
};
​
class Student : public Person
{
public:
    ~Student() { cout << "~Student()" << endl; }
};
​
int main()
{
    Person* pp = new Person;
    delete pp;  
    // ~Person()
​
    pp = new Student;
    delete pp;
    // ~Person() --> 说明没有调用派生类的析构函数
    return 0;
}

不过解决方法很简单,只要把基类的析构函数设置为虚函数,然后在派生类中重写基类的虚函数即可

但问题是基类和派生类的析构函数的函数名是不可能相同的,违背了虚函数重写的规则,答案则是 C++ 编译器对它们的名称做了特殊的处理,编译后统一处理成 destructor

#include <iostream>
using namespace std;
​
class Person
{
public:
    virtual ~Person() { cout << "~Person()" << endl; }
};
​
class Student : public Person
{
public:
    virtual ~Student() { cout << "~Student()" << endl; }
};
​
int main()
{
    Person* pp = new Person;
    delete pp;  
    // ~Person()
​
    pp = new Student;
    delete pp;
    // ~Student()
    // ~Person()
    return 0;
}


三、C++11 的 override 和 final 关键字

C++11 的 override 和 final 关键字能让我们的程序在继承类和重写虚函数时更安全,以及更清晰。

  1. override 关键字可以让编译器检查我们在派生类中重写的基类的虚函数是否正确

    class Person
    {
    public:
        virtual void func() const { cout << "hello world~" << endl; };
    };
    ​
    class Student : public Person
    {
    public:
        virtual void func() override { cout << "你好,世界~" << endl; };
    };

    这是因为我们在派生类中重写基类的虚函数时,忘记加 const

  2. final 关键字则可以防止派生类重写基类的虚函数

    class Person
    {
    public:
        virtual void func() const final { cout << "hello world~" << endl; }
    };
    ​
    class Student : public Person
    {
    public:
        virtual void func() const { cout << "你好,世界~" << endl; }
    };

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

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

相关文章

微信扫码跳转微信小程序

一:首先声明为什么需要这样做 项目中需要在后台管理页面进行扫码支付,其他人弄了微信小程序支付,所以就需要挑战小程序进行支付,在跳转的时候需要参数例如订单编号等 二:跳转小程序的方法有多种 接口调用凭证 | 微信开放文档 具体可以参考微信开放文档 1.获取scheme码 按照文…

Spring security报栈溢出几种可能的情况

今天在运行spring security的时候&#xff0c;发现出现了栈溢出的情况&#xff0c;总结可能性如下&#xff1a; 1.UserDetailsService的实现类没有加上Service注入到容器中&#xff0c;导致容器循环寻找UserDetailsService的实现类&#xff0c;最终发生栈溢出的现象。 解决方法…

工业总线与工业以太网通信协议性能评估与比较

在现代工业自动化领域&#xff0c;通信协议是实现设备间高效通信的关键。工业总线和工业以太网是两种常见的工业通信协议&#xff0c;它们在性能和适用场景方面各有优势。本文将对工业总线和工业以太网的性能进行评估与比较&#xff0c;探讨其传输速率、实时性、可靠性等指标&a…

短视频矩阵源码saas开发搭建

一、 短视频矩阵系统源码开发部署步骤分享 确定开发环境&#xff1a;务必准备好项目的开发环境&#xff0c;包括操作系统、IDE、数据库和服务器等。 下载源码&#xff1a;从官方网站或者Github等平台下载短视频矩阵系统源码&#xff0c;并进行解压。 配置数据库&#xff1a;根…

数据结构之树型结构

相关概念树的表示二叉树二叉树性质二叉树储存 实现一颗二叉树创建遍历&#xff08;前中后序&#xff09;获取树中节点个数获取叶子节点个数获取第k层节点个数获取二叉树高度检测值为value元素是否存在层序遍历&#xff08;需要队列来实现&#xff09;判断是否为完全二叉树&…

Day48|leetcode 198.打家劫舍、213.打家劫舍II、打家劫舍|||

leetcode 198.打家劫舍 题目链接&#xff1a;198. 打家劫舍 - 力扣&#xff08;LeetCode&#xff09; 视频链接&#xff1a;动态规划&#xff0c;偷不偷这个房间呢&#xff1f;| LeetCode&#xff1a;198.打家劫舍_哔哩哔哩_bilibili 题目概述 你是一个专业的小偷&#xff0c;…

Java实现根据按图搜索商品数据,按图搜索获取1688商品详情数据,1688拍立淘接口,1688API接口封装方法

要通过按图搜索1688的API获取商品详情跨境属性数据&#xff0c;您可以使用1688开放平台提供的接口来实现。以下是一种使用Java编程语言实现的示例&#xff0c;展示如何通过1688开放平台API获取商品详情属性数据接口&#xff1a; 首先&#xff0c;确保您已注册成为1688开放平台…

Android工具条

在底层&#xff0c;所有通过主题得到应用条的活动都使用ActionBar类实现它的应用条。不过最新的应用条特性已经增加到AppCompat支持库中的Toolbar类。这意味着&#xff0c;如果你想在应用中使用最新的应用条特性&#xff0c;就需要使用支持库中的ToolBar类。 如何增加工具条 1…

C++中数组作为参数进行传递方法

文章目录 基础&#xff1a;数组作为函数形参示例&#xff1a;1、一维数组的传递&#xff08;1&#xff09;直接传递&#xff08;2&#xff09;指针传递&#xff08;3&#xff09;引用传递 2、二维数组的传递&#xff08;1&#xff09;直接传递&#xff08;2&#xff09;指针传递…

【产品规划】优先级规划

文章目录 1、功能优先级保障了产品在最短时间接受验证2、隐藏在优先级背后的是产品的目标和价值3、敏捷方法论中的功能优先级制定方法4、优先级制定时常见问题和应对方法5、敏捷方法论中的开发计划制定 1、功能优先级保障了产品在最短时间接受验证 2、隐藏在优先级背后的是产品…

C语言数值表示——进制、数值存储方式

进制 进制也就是进位制&#xff0c;是人们规定的一种进位方法对于任何一种进制—X进制&#xff0c;就表示某一位置上的数运算时是逢X进一位 十进制是逢十进一&#xff0c;十六进制是逢十六进一&#xff0c;二进制就是逢二进一&#xff0c;以此类推&#xff0c;x进制就是逢x进位…

Git基础教程-常用命令整理:学会Git使用方法和错误解决

目录 一、了解Git的基本概念 二、Git的安装和配置 Git的安装 Git的配置 用户信息 文本编辑器 差异分析工具 查看配置信息 三、Git的基本操作 基本原理 基本操作命令 基本操作示例 场景一&#xff1a;创建新仓库 场景二&#xff1a;拉取并编辑远程仓库 四、常见问…

Java“牵手”1688商品跨境属性数据,1688API接口申请指南

1688平台商品详情跨境属性数据接口是开放平台提供的一种API接口&#xff0c;通过调用API接口&#xff0c;开发者可以获取1688商品的标题、价格、库存、月销量、总销量、库存、详情描述、图片&#xff0c;重量&#xff0c;详情描述等详细信息 。 获取商品详情接口API是一种用于…

《Linux内核源码分析》(3)调度器及CFS调度器

《Linux内核源码分析》(3)调度器及CFS调度器 文章目录 《Linux内核源码分析》(3)调度器及CFS调度器一、调度器1、调度器2、调度类sched_class结构体3、优先级4、内核调度策略 二、CFS调度器1、CFS调度器基本原理2、调度子系统各个组件模块3、CFS调度器就绪队列内核源码 一、调度…

影响屏蔽箱使用效果的因素有哪些?

屏蔽箱到底是用来屏蔽什么的&#xff1f;屏蔽箱主要是为无线制造行业&#xff0c;如手机、平板、遥控器、无线网卡、蓝牙设备、路由器、GPS行业、智能家居等提供隔离测试环境&#xff0c;屏蔽外界对被测产品的干扰&#xff0c;让信号经过特殊处理和被测产品通讯。 影响屏蔽效果…

Java“牵手”1688商品详情数据,1688API接口申请指南

1688平台商品详情接口是开放平台提供的一种API接口&#xff0c;通过调用API接口&#xff0c;开发者可以获取1688商品的标题、价格、库存、月销量、总销量、库存、详情描述、图片等详细信息 。 获取商品详情接口API是一种用于获取电商平台上商品详情数据的接口&#xff0c;通过…

【C++】详解声明和定义

2023年8月28日&#xff0c;周一下午 研究了一个下午才彻底弄明白... 写到晚上才写完这篇博客。 目录 声明和定义的根本区别结构体的声明和定义声明结构体 定义结构体类的声明和定义函数的定义和声明声明函数 定义函数变量声明和定义声明变量定义变量 声明和定义的根本区别 …

CSS学习笔记01

CSS笔记01 什么是CSS CSS&#xff08;Cascading Style Sheets &#xff09;&#xff1a;层叠样式表&#xff0c;也可以叫做级联样式表&#xff0c;是一种用来表现 HTML 或 XML 等文件样式的计算机语言。字体&#xff0c;颜色&#xff0c;边距&#xff0c;高度&#xff0c;宽度…

掌握C/C++协程编程,轻松驾驭并发编程世界

一、引言 协程的定义和背景 协程&#xff08;Coroutine&#xff09;&#xff0c;又称为微线程或者轻量级线程&#xff0c;是一种用户态的、可在单个线程中并发执行的程序组件。协程可以看作是一个更轻量级的线程&#xff0c;由程序员主动控制调度。它们拥有自己的寄存器上下文…