多态的基本使用

news2024/11/23 23:29:07
这部分的内容主要是记住使用方法,原理在之后会讲。
多态的概念
多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。
比如说买票,普通人买票就是正常买,学生可以买学生票,军人可以优先买票。对于同一个行为,不同的对象去完成会产生不同的结果。即不同的对象去调用这个函数(买票),会得到不同的结果。
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。实现多态首先要存在被继承的基类,其次还存在两个条件:
1. 必须通过基类的指针或者引用调用虚函数
2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写(也叫覆盖)
虚函数是被virtual修饰的成员函数,修饰方法是在函数返回值前加上virtual。
虚函数的重写:派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称子类的虚函数重写了基类的虚函数
示例:
class person
{
public:
        virtual void BuyTicket()  {   cout << "普通人 全价-100" << endl;   }
private:
        string _name;
};
class student : public person
{
public:
//基类和派生类的成员函数前面如果都没有加上virtual,派生类函数名和基类相同,构成隐藏,要调用基类函数要声明作用域。
//前面加上了virtual,且函数名,函数参数,返回值相同,构成重写(也叫覆盖)
        virtual void BuyTicket()   {   cout << "学生 半价-50" << endl;   }
};
class soldier : public person
{
public:
        virtual void BuyTicket() {   cout << "军人 优先买票 全价-100" << endl;  }
};
下面来看看应用场景,添加如下代码:
void Pay(person* ptr)
{
        ptr->BuyTicket(); //运行后发现,虽然都是person类型的指针调用了BuyTicket函数,但是运行的结果不一样
        cout << endl;
}
int main()
{
        person p;
        student st;
        soldier so;
        int option = 0;
        do
        {
               cout << "请输入:";
               cout << "1:学生  2:军人  3:普通人" << endl;
               cin >> option;
               switch (option)
               {
                       case 1:
                              Pay(&st); //只要传不同类型的对象,就可以达到不同的行为
                              break;
                       case 2:
                              Pay(&so);
                              break;
                       case 3:
                              Pay(&p);
                              break;
                       default:
                              cout << "输入错误,请重新输入" << endl;
               }
        } while (option != -1);
        return 0;
}
运行结果:
将内容变的稍微复杂一点:
class person
{
public:
        person(string name)
               :_name(name)
        {}
        virtual void BuyTicket()
        {
               cout << _name <<" 普通人 全价-100" << endl;
        }
protected: //需要被继承的成员对象要定义成公有或者保护
        string _name;
};
class student : public person
{
public:
        student(string name)
               :person(name)
        {}
        virtual void BuyTicket()
        {
               cout << _name << " 学生 半价-50" << endl;
        }
};
class soldier : public person
{
public:
        soldier(string name) //基类没有默认构造函数,就需要自己显式调用基类构造函数,完成基类部分的初始化
               :person(name)
        {}
        virtual void BuyTicket()
        {
               cout << _name << " 军人 优先买票 全价-100" << endl;
        }
};
void Pay(person* ptr)
{
        ptr->BuyTicket();
        delete ptr;
        cout << endl;
}
int main()
{
        int option = 0;
        do
        {
               cout << "请输入:";
               cout << "1:学生  2:军人  3:普通人" << endl;
               cin >> option;
               string name;
               cout << "请输入姓名:";
               cin >> name;
               switch (option)
               {
                       case 1:
                              Pay(new student(name)); //不能通过匿名对象完成传参,原因不明
                              break;
                       case 2:
                              Pay(new soldier(name));
                              break;
                       case 3:
                              Pay(new person(name));
                              break;
                       default:
                              cout << "输入错误,请重新输入" << endl;
               }
        } while (option != -1);
        return 0;
}
多态的实现存在两个条件(需要背下来):
1.派生类虚函数重写基类虚函数(重写的条件:返回值,函数名,函数参数相同+虚函数)
2.基类指针或者引用调用虚函数
这两个条件中的任意一点不满足都会导致无法实现多态。
这些条件和虚函数表有关,会在多态原理中了解,目前只需要记住就可以了。
当然,存在一些看似不符合条件,实际上能符合条件的情况
1.协变:对返回值要求的例外,如果返回值满足派生类的返回值(B*)为其基类返回值(A*)的派生类,则视为满足返回值相同。返回值类型必须是指针或对象引用。
class A{};
class B : public A {};
class Person
{
public:
    virtual A* f() 
    {
        cout << "virtual A* f() " << endl;
        return nullptr;
    }
};
class Student : public Person
{
public:
    virtual B* f() 
    {
        cout << "virtual B* f() " << endl;
        return nullptr;
    }
};
int main()
{
    Person p;
    Student s;
    Person* ptr;
    ptr = &p;
    ptr->f();
    ptr = &s;
    ptr->f();
    return 0;
}
2.派生类虚函数没有写virtual依然被认为是虚函数,因为先继承了基类的函数接口声明,重写了基类虚函数的实现。具体见最后。
我们自己写代码的时候要在派生类中加上virtual的,不加virtual是一种很不好的习惯。
析构函数的重写
class Person 
{
public:
    virtual ~Person() {cout << "~Person()" << endl;}
};
class Student : public Person
{
public:
    virtual ~Student() { cout << "~Student()" << endl; }
};
int main()
{
    Person p;
    Student S;
    return 0;
}
按照先定义的后析构,先子后父的顺序,完成析构。加上virtual也一样,所以构不构成重写不影响普通对象的析构顺序
如果基类没有virtual,就不会构成多态,那么ptr->destructor()就是普通的函数调用,无法构成多态,也就不会调用student的析构函数,student的内存没有释放会导致内存泄漏。
关于destructor,编译器会把所有类的析构函数处理成destructor的形式,就是为了满足此时多态的需求(虚函数+函数名,函数参数,返回值相同),只有满足多态了,才能保证ptr = new Student; delete ptr能释放合适的空间(delete是根据指针类型来调用析构函数释放空间的,只有构成多态才能保证基类指针调用派生类析构函数)。所以在继承的部分,在派生类的析构函数中调用基类的析构函数且不声明作用域会报错(虽然派生类会在结束自动调用基类的析构函数,不需要再调用,但这里只是解释一下为什么在派生类调用基类的析构函数会报错,即使派生类和基类都没有成员变量(有成员变量就可能需要释放空间,对一个空间释放两次就会报错)),两种析构函数都被处理成destructor的形式了,会导致无限递归。
如果设计一个可能被继承的类,首先,成员函数、成员变量最好定义为保护;其次,析构函数最好定义为虚函数。这样能保证析构函数被正常的析构。
整个机制的逻辑关系:继承->继承的切片->基类指针可能指向派生类->delete基类指针只能调用基类析构函数->将析构函数都处理成destructor()->满足多态条件->完成基类指针调用派生类析构函数
c++11 override和final
从上面可以看出,C++对函数重写的要求比较严格,但是有些情况下由于疏忽,可能会导致函数名字母次序写反而无法构成重载,而这种错误在编译期间是不会报出的,只有在程序运行时没有得到预期结果才来debug会得不偿失,因此:C++11提供了override和fifinal两个关键字,可以帮助用户检测是否重写。
final:修饰虚函数,表示虚函数不可被重写
class Car
{
public:
    virtual void Drive() final {} //final的使用方式
};
class Benz :public Car
{
public:
    virtual void Drive() {cout << "Benz-舒适" << endl;}
};
上面这部分代码会在编译时报错。
之前有提到过实现一个不可被继承的类,当时的方法是将构造函数私有化,但是这是间接的不可继承,如果没有创建派生类,那么,即使派生类继承了基类,也不会报错。而final可以使类直接的不可继承,即在类名后加上final,示例:class Car final。表示最终类,不可被继承。
override:检查派生类的虚函数是否重写了基类的某个虚函数,如果没有重写则编译报错。
class Car
{
public:
    virtual void Drive(){}
};
class Benz :public Car
{
public:
    virtual void Drive() override  //override的使用方式
        {cout << "Benz-舒适" << endl;}
};
override一定是写在派生类当中的。
重载,重写,重定义三者的区别:
抽象类
在虚函数的后面写上 =0 ,则这个函数为纯虚函数(纯虚函数是虚函数的一种)。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数的性质规定了派生类必须重写,否则无法实例化出对象,另外纯虚函数更体现出了接口继承。
class Car
{
public:
    virtual void Drive() = 0; //纯虚函数的实现没有意义,因为没有对象能调用。声明一下,继承接口就可以了。
};
一般在现实中没有具体实体的类会被定义成抽象类。
虽然抽象类无法实例化出对象,但可以定义指针,通过指针完成多态,比如下面这种情况:
class Benz :public Car
{
public:
    virtual void Drive()
    {
        cout << "Benz-舒适" << endl;
    }
};
class BMW :public Car
{
public:
    virtual void Drive()
    {
        cout << "BMW-操控" << endl;
    }
};
int main()
{
    Car* pBenz = new Benz;
    pBenz->Drive();
    Car* pBMW = new BMW;
    pBMW->Drive();
    return 0;
}
在没有指明具体是什么车时,车是一个抽象的概念。指明具体对象后就能通过多态展现具体的特点。
如果在BMW的成员函数中加上参数,则new BMW会报错,这暗示BMW继承了car的纯虚函数,即使构成了隐藏,但car中的纯虚函数还是在BMW类中了,导致BMW变成了抽象类,无法实例化出对象。
接口继承和实现继承
普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现(抽象类中的普通函数,未被重写的虚函数,也可以被实现继承)。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口(所以没有写virtual也会被认定是虚函数),目的是为了重写,达成多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。经过实际检验,没有重写的接口继承会变成实现继承。
下面是一道选择题:
答案是B
所以只要派生类返回值,函数名,函数参数与基类相同,就会继承接口,构成重写,就能实现多态。凡是实现接口继承的,必能实现多态。

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

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

相关文章

WTM框架页面被其他网站引用免登录

用ASP.NET CORE开发通常都会有这样一个需求&#xff0c;自己框架开发的页面&#xff0c;要被其他网站嵌套引用&#xff0c;但其他网站通过链接到自己的开发页面的时候&#xff0c;通常会有一个登录页面&#xff0c;有的时候网站无缝集成的时候&#xff0c;这就会要求跳过这个WT…

前端实现 DIV 高度只有100px,宽度只有100px ,我要在这个DIV放一个宽度200的DIV,左右拉动滚动条显示

<!DOCTYPE html> <html> <head><title>点击监听两组span标签</title><style>.outer-div {width: 100px;height: 100px;overflow-x: scroll;background-color: #abc1ee;}.inner-div {width: 200px;}/* 自定义滚动条样式 */.outer-div::-web…

Java 的集合

一、Collection 1、ArrayList 底层采用数组实现&#xff0c;操作大多基于对数组的操作。 在添加和删除时需要做 System.arraycopy(native层方法) 拷贝工作。 添加元素时可能会扩容&#xff0c;这要大量的拷贝工作&#xff0c;删除元素时&#xff0c;会把后面的元素向前拷贝。…

剑指oferr68-II.二叉树的最近公共祖先

为什么这道题的难度是easy&#xff0c;我感觉挺难的啊&#xff0c;我想了挺久没有一点思路就直接看题解了。题解有两种解法&#xff0c;先看第一种存储父节点 class Solution {Map<Integer,TreeNode> parent new HashMap<Integer,TreeNode>();Set<Integer>…

ffmpeg2段视频合成一段

查看分辨率 帧率和编码器 ffprobe -v error -select_streams v:0 -show_entries streamcodec_name,width,height,avg_frame_rate -of defaultnoprint_wrappers1 rs2.mp4得到&#xff0c;编码器&#xff0c;分辨率&#xff0c;还有帧率 codec_nameh264 width1920 height1080 avg…

小白到运维工程师自学之路 第五十二集 (三剑客之awk)

一、概述 awk命令是一种在Unix或类Unix系统上使用的文本处理工具。它以行为单位读取输入文件&#xff0c;按照预定义规则对每一行进行处理并生成输出。 通过使用一种简单的编程语言&#xff0c;awk提供了对文本数据进行过滤、处理和转换的强大能力。它可以轻松地提取文本中的…

实验四 交换机 VLAN 配置

文章目录 实验目的实验原理实验内容实验总结 实验目的 理解 VLAN 的概念、原理&#xff1b;掌握基于交换机端口的 VLAN 划分方法&#xff1b;掌握 Cisco2950 交换机的单交换机和跨交换机 VLAN 配置方法。 实验原理 &#xff08;1&#xff09;VLAN 的概念 VLAN&#xff08;Vi…

CQ 社区版 2.2.0 发布 | 配置要求降为 4 核 16G!!!

&#x1f389;&#x1f389;&#x1f389; 喜大普奔&#xff01;&#xff01;&#xff01; 经过我们研发团队的不断努力&#xff0c;CloudQuery 终于「瘦身」成功啦&#xff01;&#xff01;&#xff01; 本次发布的 v2.2.0 版本&#xff0c;推荐配置降为&#xff1a; CPU&a…

【iOS内存管理-内存的几大区域】

前言 iOS内存管理的第一篇章&#xff0c;了解iOS内存的五大分区。 总览 iOS中&#xff0c;内存主要分为五大区域&#xff1a;栈区&#xff0c;堆区&#xff0c;全局区/静态区&#xff0c;常量区和代码区。总览图如下。 如上图所示&#xff0c;代码区是在低地址段存放&#x…

关于AWS MSK Connector Execution Role的解释

尽管在创建AWS MSK Connector时&#xff0c;Execution Role是一个必填项&#xff0c;就像下面这样&#xff1a; 并且在官方文档中给出的Execution Role样例&#xff1a; https://docs.aws.amazon.com/msk/latest/developerguide/msk-connect-service-execution-role.html 中也…

基于JSP+Servlet的学生信息管理系统

用户类型&#xff1a;三角色&#xff08;学生、教师、管理员&#xff09; 项目架构&#xff1a;B/S架构 设计思想&#xff1a;MVC 开发语言&#xff1a;Java语言 前端技术&#xff1a;Layui、HTML、CSS、JS、JQuery、Ajax等技术 后端技术&#xff1a;JSP、Servlet、JDBC、…

基于JSP+Servlet的医药药品管理系统

用户类型&#xff1a;双角色角色&#xff08;患者、管理员[医生]&#xff09; 设计模式&#xff1a;MVC&#xff08;jspservletjavabean) 项目架构&#xff1a;B/S架构 开发语言&#xff1a;Java语言 主要技术&#xff1a;jsp、servlet、jdbc、jsp、html5、jquery、css、js…

【哈希表/字符串-简单】LeetCode 205 同构字符串 Java

需要判断s和t每个位置上的字符是否都一一对应&#xff0c;即s的任意一个字符被t中唯一的字符对应&#xff0c;同时t的任意一个字符被s中唯一的字符对应。这也被称为【双射】的关系。 以示例二为例&#xff0c;t中的字符a和r虽然有唯一的映射o&#xff0c;但对于s中的字符o来说…

Vim批量注释与反注释

在使用vim编写代码的时候&#xff0c;经常需要用到批量注释与反注释一段代码。下面简要介绍其操作。本文记录在mac/linux下的vim批量注释。 一开始我想让vim配置ctrl/快捷键&#xff0c;快速批量注释&#xff0c;但是vim的文档中不支持这样的快捷键。 如果实在要弄也能弄&…

集群基础2——LVS负载均衡httpd服务(nat模式)

文章目录 一、环境说明一、配置调度器网卡二、配置后端服务器三、配置调度器四、验证五、设置https负载均衡 一、环境说明 使用lvs中的nat模型&#xff0c;对http负载均衡集群。 主机IP角色安装服务真实IP&#xff1a;192.168.161.129VIP&#xff1a;192.168.161.130调度服务器…

深信服 网络工程师面试题(二)

指针函数和函数指针的区别 指针函数是指带指针的函数&#xff0c;即本质是一个函数&#xff0c;函数返回类型是某一类型的指针。 首先它是一个函数&#xff0c;只不过这个函数的返回值是一个地址值。函数返回值必须用同类型的指针变量来接受&#xff0c;也就是说&#xff0c;指…

牛客HJ99 - 自守数【暴力 + 换位取模】

原题传送门 原题描述 首先我们来看一下原题是怎么描述的&#xff0c;题面很简单&#xff0c;输入n&#xff0c;然后让我们去统计从1 ~ n之间的自守数有几个&#xff0c;那什么是【自守数】呢&#xff0c;上面也说到了&#xff0c;即一个数在平方之后该数的尾数等于该数自身的自…

榜单!高精定位模块/系统「争夺战」,份额Top5供应商都有谁

以当前国内车企落地高速NOA采用的主流方案为例&#xff0c;普遍采用了「高精定位高精地图车端传感器」的多源融合定位策略。其中&#xff0c;在高精定位部分&#xff0c;大部分采用GNSSIMURTK的方案。 从目前的智驾系统演进来看&#xff0c;主流的仍是在L2基础上&#xff0c;通…

k8s 基础命令和常用命令等

通过kubectl命令可以操作和管理K8S资源&#xff0c;对于初学者可以在掌握K8S基础命令的基础上再去学习K8s的原理和架构&#xff0c;那么K8S常用的命令有哪些呢&#xff1f; 01 K8S命令概述 在学习K8s基础命令前&#xff0c;了解和学习docker命令是很有必要的&#xff0c;kub…

c语言--unsigned修饰符

在C语言中&#xff0c;unsigned是一种无符号整数修饰符。它可以与多个整数类型&#xff08;如int、short、long等&#xff09;结合使用&#xff0c;表示该整数类型只包含非负数值。unsigned修饰的整数类型不保存正负号位&#xff0c;因此可以用来表示更大的正整数范围。 例如&…