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

目录

一、多态的概念和虚函数

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

微信扫码跳转微信小程序

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

【项目实战典型案例】05.前后端分离的好处(发送调查问卷)

目录 一、背景二、思路三、过程1、主要的业务逻辑2、解决问题的思路 四、总结五、面向对象的好处 一、背景 以下流程图是给用户发送调查问的整体流程&#xff0c;将不必要的业务逻辑放到前端进行处理。这样导致逻辑混乱难以维护。前后端分离的其中一个目的是将功能的样式放在了…

基础论文学习(5)——MAE

MAE&#xff1a;Masked Autoencoders Are Scalable Vision Learners Self-Supervised Learning step1&#xff1a;先用无标签数据集&#xff0c;把参数从一张白纸训练到初步预训练模型&#xff0c;可以得到数据的 Visual Representationstep2&#xff1a;再从初步成型&#x…

clickhouse ssb-dbgen数据构造 及 clickhouse-benchmark简单压测

一、 测试数据构造 1. 数据样例 官方文档有给出一批数据样例。优点是比较真实&#xff0c;缺点是太大了&#xff0c;动辄上百G不适合简单小测试 Anonymized Yandex.Metrica DatasetStar Schema BenchmarkWikiStatTerabyte of Click Logs from CriteoAMPLab Big Data Benchma…

浅析Linux 物理内存外碎片化

本文出现的内核代码来自Linux4.19&#xff0c;如果有兴趣&#xff0c;读者可以配合代码阅读本文。 一、Linux物理内存外碎片化概述 什么是Linux物理内存碎片化&#xff1f;Linux物理内存碎片化包括两种&#xff1a; 1.物理内存内碎片&#xff1a;指分配给用户的内存空间中未…

【产品规划】优先级规划

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

C++ list模拟实现

list模拟实现代码&#xff1a; namespace djx {template<class T>struct list_node{T _data;list_node<T>* _prev;list_node<T>* _next;list_node(const T& x T()):_data(x),_prev(nullptr),_next(nullptr){}};template<class T,class Ref,class Pt…

ctfshow-红包题第二弹

0x00 前言 CTF 加解密合集CTF Web合集 0x01 题目 0x02 Write Up 同样&#xff0c;先看一下有没有注释的内容&#xff0c;可以看到有一个cmd的入参 执行之后可以看到文件代码&#xff0c;可以看到也是eval&#xff0c;但是中间对大部分的字符串都进行了过滤&#xff0c;留下了…

纸贵科技连续三年蝉联IDC中国 FinTech 50榜单

近日&#xff0c;国际权威市场研究机构IDC公布了“2023 IDC中国FinTech 50榜单”。作为领先的区块链技术和解决方案服务商&#xff0c;纸贵科技凭借过硬的区块链技术和丰富的金融科技创新成果&#xff0c;连续第三年荣登IDC中国FinTech 50榜单。 IDC中国FinTech 50榜单是金融科…

【leetcode 力扣刷题】双指针///原地扩充线性表

双指针///原地扩充线性表 剑指 Offer 05. 替换空格定义一个新字符串扩充字符串&#xff0c;原地替换思考 剑指 Offer 05. 替换空格 题目链接&#xff1a;剑指 Offer 05. 替换空格 题目内容&#xff1a; 这是一道简单题&#xff0c;理解题意&#xff0c;就是将字符串s中的空格…

很干的 Nginx

&#x1f3a8; 前言 本篇文章有些概念性的东西&#xff0c;是结合自己的理解表达出来的&#xff0c;可能有些理解不到位的地方。希望多多指教&#xff0c;谢谢大家。 红包献上 &#x1f9e7;&#x1f9e7;&#x1f9e7;&#x1f9e7;&#x1f9e7;&#x1f9e7;&#x1f9e7;…

【c语言】文件操作 万字详解

目录 一&#xff0c;为什么使用文件 二&#xff0c;什么是文件 1&#xff0c;程序文件 2&#xff0c;数据文件 3&#xff0c;文件名 三&#xff0c;文件的打开和关闭 1&#xff0c;文件指针 2&#xff0c;文件的打开和关闭 四&#xff0c; 文件的顺序读写 1&#xff0c;顺序…

ethers.js2:provider提供商

1、Provider类 Provider类是对以太坊网络连接的抽象&#xff0c;为标准以太坊节点功能提供简洁、一致的接口。在ethers中&#xff0c;Provider不接触用户私钥&#xff0c;只能读取链上信息&#xff0c;不能写入&#xff0c;这一点比web3.js要安全。 除了之前介绍的默认提供者d…

如何编译打包OpenSSH 9.4并实现批量升级

1 介绍 openssh 9.4版本已于8月10号发布&#xff0c;安全团队又催着要赶紧升级环境里的ssh版本&#xff0c;本文主要介绍Centos5、Centos6、Centos7下openssh 9.4源码编译rpm包以及批量升级服务器openssh版本的方法。关注公众号后台回复ssh可获取本文相关源码文件。 https://w…

使用Tampermonkey(篡改猴)向页面注入js脚本

一、Tampermonkey 简单介绍 Tampermonkey是一款浏览器插件&#xff0c;适用于Chrome、Microsoft Edge、Safari、Opera Next 和 Firefox。他允许我们自定义javascript给指定网页添加功能&#xff0c;或修改现有功能。也可以用来辅助调试&#xff0c;或去除网页广告等。 官网地…

深度学习-4-二维目标检测-YOLOv3理论模型

单阶段目标检测模型YOLOv3 R-CNN系列算法需要先产生候选区域&#xff0c;再对候选区域做分类和位置坐标的预测&#xff0c;这类算法被称为两阶段目标检测算法。近几年&#xff0c;很多研究人员相继提出一系列单阶段的检测算法&#xff0c;只需要一个网络即可同时产生候选区域并…

Redis.conf详解

Redis.conf详解 配置文件unit单位对大小写不敏感 包含 网络 bind 127.0.0.1 # 绑定的ip protected-mode yes # 保护模式 port 6379 # 端口设置通用 GENERAL daemonize yes # 以守护进程的方式运行 默认为no pidfile /var/run/redis_6379.pid #如果以后台的方式运行&#xff…

bash: conda: command not found

问题描述&#xff1a; 在Pycharm上用SSH远程连接到服务器&#xff0c;打开Terminal准备查看用 conda 创建的虚拟环境时&#xff0c;却发现调用 conda 指令时出现以下报错&#xff1a; -bash: conda: command not found如果使用Xshell 利用端口号直接连接该 docker 容器&#…

CTF-XXE(持续更新,欢迎分享更多相关知识点的题目)

知识 实例 BUU [PHP]XXE 进来看到 然后一起看 Write BUU XXE COURSE 1 进来看到 一起看 write NSS [NCTF2019]Fake XML cookbook 反正是XXE 直接整 write [NCTF 2019]True XML cookbook 不整花里胡哨&#xff0c;解题在最下面 write 与博主不同&#xff0c;我通过…

SQL中ON筛选和Where筛选的区别

转载&#xff1a;sql连接查询中on筛选与where筛选的区别https://zhuanlan.zhihu.com/p/26420938 结论:on后面接上连接条件&#xff0c;where后面接上过滤条件