【C++】多态(上) 多态 | 虚函数 | 重写 | final、override | 接口继承与实现继承 | 抽象类

news2024/11/25 6:41:02

一、多态

概念

多态,就是多种状态,即不同的对象去完成同一个行为时会产生出不同的状态。比如:买票时,成人要原价买,学生和老人就可以享受优惠价便宜一点儿。同样是买票这个行为,不同的对象来做就有不同的状态,这就是多态的一种体现。

从代码实现上来说,多态指的是通过一个父类指针 or引用调用一个虚函数时,会根据具体对象的类型来调用该虚函数的不同实现。在多态中,相同的操作可以作用于不同的对象,而具体执行的操作则取决于对不同对象的类型判断。(“看人下菜碟🤪”)

那怎么构成多态呢?有两个条件:

1.子类重写父类的虚函数

2.通过父类的指针或引用去调用虚函数

估计你现在是一头雾水:啥是虚函数?啥是重写?多态到底是怎么用的?

不急,你现在对多态的概念一定还是一片混沌。下面我先讲解“虚函数”、“重写”的概念,然后举出多态的代码实例,你才能体会什么是多态。看到后面,再回过头来看多态的概念,会有更透彻的理解。

虚函数

先来学习一个知识点“虚函数”,注意,和菱形继承那里的 虚继承 是两个完全不同的概念!它俩的关系就是金鱼和自行车之间的关系。

🎈虚函数:被virtual修饰的类成员函数称为虚函数。

虚函数必须是非静态的成员函数,非成员函数和静态函数是无法成为虚函数的。

class Person {
public:
    virtual void BuyTicket() { …… }
};

虚函数的作用是:实现多态机制。

重写(覆盖)

若子类定义了一个和父类的虚函数 一模一样的虚函数,那么称子类中的虚函数重写 / 覆盖了父类的虚函数。(拥有相同的名字、返回值、形参列表)

重写是针对虚函数的概念。普通函数是没有重写的说法的。

例:

class Person 
{
public:
    virtual void BuyTicket() {
        cout << "成人票10r" << endl;
    }
};
class Student:public Person
{
public:
    virtual void BuyTicket() {    //子类中的虚函数重写了父类的虚函数
        cout << "学生票5r" << endl;
    }
};

其实,在重写基类虚函数时,派生类的虚函数即使 不加virtual关键字,也是可以构成重写的

这是因为,继承后基类的虚函数被继承下来了,在派生类依旧保持虚函数属性。

但是该种写法不规范,不建议这样使用。我们还是老老实实加上virtual吧。

如何构成多态

构成多态的两个条件:1.子类重写父类的虚函数 2.通过父类的指针或引用去调用虚函数

我们已经有了“虚函数”和“重写”的知识储备,现在我来写一个多态的例子:

#include<iostream>
using namespace std;
class Person 
{
public:
    virtual void BuyTicket() {
        cout << "成人票10r" << endl;
    }
};
class Student:public Person
{
public:
    virtual void BuyTicket() {    //条件1.子类重写父类的虚函数
        cout << "学生票5r" << endl; 
    }
};
void Pay(Person& p) {   
    p.BuyTicket();    //条件2.通过父类的引用去调用虚函数
}
int main() {
    Person p;
    Student s;
    //用引用调用
    Pay(p);
    Pay(s);
    cout << endl;
    //用指针调用
    Person* p1 = &p, * p2 = &s;   //条件2.通过父类的指针去调用虚函数
    p1->BuyTicket();
    p2->BuyTicket();
    return 0;
}

这,就是多态!

Q:

为什么调虚函数 一定得是父类的指针or引用?子类不行吗?

因为父类不仅能接收父类的值,还能接收子类的值;而子类仅能接收子类的值,不能接收父类的值。也就是说,父类更能包罗。

为什么一定得通过指针or引用调用?

这个问题先搁置一下。等讲到多态的底层原理时再说。

重写的例外:协变

(这个知识点实际上不常用,但是笔试题会考)

协变:是函数重写的一种特殊情况,这种情况下,父类和子类的虚函数的返回值是不同的

父类的虚函数返回父类对象的指针or引用,子类的虚函数返回子类对象的指针or引用。这种情况就称为“协变”。

协变的前提是:协变的两个类型必须是父子关系。

例:

class Person 
{
public:
    virtual Person& BuyTicket() {   //注意返回类型
        Person p;
        cout << "成人票10r" << endl;
        return p;
    }
};
class Student:public Person
{
public:
    virtual Student& BuyTicket() {   //注意返回类型
        Student s;
        cout << "学生票5r" << endl;
        return s;
    }
};
int main() {
    Person p;
    Student s;
    Person* p1 = &p, * p2 = &s;
    p1->BuyTicket();
    p2->BuyTicket();
    return 0;
}

既然存在返回值协变的情况,那说明构成多态的虚函数 的返回类型未必相同。这是常见的考点。

析构函数的重写

析构函数是需要重写的。

➡️为什么呢?来看下面这种特殊的情况:

//当析构函数不是虚函数,也未被重写时
class A
{
public:
    ~A() {   
        cout << "~A()" << endl;
    }
};
class B:public A
{
public:
    ~B() {
        cout << "~B()" << endl;
    }
};
int main() {
    A* pa = new B;  
    delete pa;    //我希望调用的是B的析构函数
    return 0;
}

当我用一个父类的指针去指向子类的对象时,我希望 程序能根据对象的类型去调用对应的析构函数,也就是~B()。

但实际上,这里并没有实现多态,程序会老老实实根据pa的类型去调用~A()。而这样,由于B中部分成员未被释放,如果这部分成员涉及资源管理的话,就会导致内存泄露。

所以,析构函数得实现多态。

(这种特定情况需要记住,面试时经常会问到这个问题)

➡️但问题又来了:实现多态的前提是 子类重写父类的虚函数,而这俩的析构函数名字都不一样,怎么能重写呢?

这就不用我们担心了,编译器会出手。可以理解为,编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor,我们只要加上virtual就可以。

正确的写法:

class A
{
public:
    virtual ~A() {
        cout << "~A()" << endl;
    }
};
class B:public A
{
public:
    virtual ~B() {
        cout << "~B()" << endl;
    }
};
int main() {
    A* pa = new B;
    delete pa;
    return 0;
}

关键字final(C++11)

final,“最终的”,表示这个就是最终版,后继无人了。

final的两个功能:1. 修饰虚函数,表示该虚函数不能再被重写 2. 修饰类,让此类不能被继承

//修饰虚函数
class A
{
public:
    virtual void print() final {   
        ……
    }
};
class B:public A
{
public:
    ……  //不能继承print函数
};

之前我们说过,要想一个类不能被继承,那就把它的构造函数私有化。实际上,这种方式并不好。因为从继承关系来说,它并没有直接阻止继承这种行为,子类依旧是继承了父类。只是创建创建不了子类对象。这是一种间接的阻止。

C++11引入的final,就可以直接阻止,只要父类加了final,那就是不可被继承的:

class A final   
{
public:
    ……
};
​
class B:public A   //会报错"不能将final类类型作为基类"
{
public:
    ……
};

关键字override(C++11)

override: 检查子类虚函数 是否正确地完成重写,如果没有则编译报错。

有时候我们会不小心把函数名写错,或者父子的参数列表顺序不一致,导致无法构成重写。这种错误编译器往往检查不出来,当运行出错误的结果时,我们去Debug半天,才无语地发现原来bug在这。

而override的出现,就是帮助我们去检查错误,确保重写的正确,免得我们调试找BUG。

class A 
{
public:
    virtual void print() {
        ……
    }
};
class B:public A
{
public:
    void pint() {   //这里把函数名写错了,但编译器没检查出来
        ……
    }
};

当加了override以后:

class B:public A
{
public:
    void pint() override {  
        ……
    }
};

接口继承和实现继承

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现

虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口。因为目的是重写,达成多态,所以只需要继承接口,函数体就用不着了。

所以说,如果不实现多态,不要把函数定义成虚函数。

重载、重写、重定义的对比

二、抽象类

概念

在虚函数的后面写上 =0 ,则这个函数为纯虚函数,纯虚函数是不需要实现的。

包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象,只能被继承。

派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。

class Course    //课程是抽象类
{
public:
    virtual void TeachingMethod() = 0;  //纯虚函数
};
class Math:public Course
{
public:
    virtual void TeachingMethod() {
        cout << "Math" << endl;
    }
};
int main() {
    //Course c;   //报错,抽象类无法实例化出对象
    Math m;      //派生类进行了重写,可以实例化出对象
    return 0;
}

纯虚函数的作用是:强制子类去完成重写。

抽象类用于表示抽象的类型。实际中,许多概念是抽象的,没有对应的实体。比如“植物” “形状”,这是笼统的概念,只有把“植物”具体到“含羞草”,把“形状”具体到“三角形”,才能定义出一个具体的类型。

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

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

相关文章

招募引流模式是实体门店吸引顾客的一种有效策略

在如今激烈的市场竞争和庞大的客户需求中&#xff0c;应该采取什么样的方式来应对&#xff0c;才能找到自己的一席之地。招募引流模式是实体门店吸引顾客的一种有效策略&#xff0c;通常招募体验官或合作伙伴&#xff0c;让他们协助门店进行推广活动&#xff0c;达到增加客流量…

Go——三、运算符以及流程控制

Go 一、Go语言运算符1、算数运算符2、关系运算符3、逻辑运算符4、位运算符5、赋值运算符6、其他运算符7、运算符优先级 二、Go的流程控制1、if else2、for 循环结构3、for range&#xff08;键值循环&#xff09;4、switch case5、break&#xff1a;跳出循环6、go&#xff1a;跳…

AIGC系列之:DDPM原理解读(简单易懂版)

目录 DDPM基本原理 DDPM中的Unet模块 Unet模块介绍 Unet流程示意图 DownBlock和UpBlock MiddleBlock 文生图模型的一般公式 总结 本文部分内容参考文章&#xff1a;https://juejin.cn/post/7251391372394053691&#xff0c;https://zhuanlan.zhihu.com/p/563661713&…

Selenium 连接到现有的 Firefox 示例

当前环境&#xff1a; python 3.7 selenium 3.14.1 urllib3 1.26.8 Frefox 115.1.0esr(32位) geckodriver.exe 0.33.0 1 下载 Firefox 浏览器&#xff0c;根据自己的需要选择。 下载 Firefox 浏览器&#xff0c;这里有简体中文及其他 90 多种语言版本…

为什么对中小企业来说,数字化转型很难?

通过整合尖端数字技术和创造性流程&#xff0c;实现公司运营和客户参与的现代化&#xff0c;被称为“数字化转型”。在当今瞬息万变的商业环境中&#xff0c;数字化转型已成为中小型企业寻求生存和可持续增长的关键要求。拥抱数字化转型对于企业的长期成功和可持续发展至关重要…

世微AP5125 DC-DC降压恒流 LED车灯电源驱动IC SOT23-6

产品描述 AP5125 是一款外围电路简单的 Buck 型平均电流检测模式的 LED 恒流驱动器&#xff0c;适用于 8-100V 电压范围的非隔离式大功率恒流 LED 驱动领域。芯片采用固定频率 140kHz 的 PWM 工作模式&#xff0c; 利用平均电流检测模式&#xff0c;因此具有优异的负载调整 率…

Day49:647. 回文子串、516.最长回文子序列

文章目录 647. 回文子串思路代码实现 516.最长回文子序列思路代码实现 647. 回文子串 题目链接 思路 确定dp数组&#xff08;dp table&#xff09;以及下标的含义 布尔类型的dp[i][j]&#xff1a;表示区间范围[i,j] &#xff08;注意是左闭右闭&#xff09;的子串是否是回文…

个微协议/ipad协议/协议/8.0.37协议

微信开发API接口WX/ipad/8.0.37协议&#xff0c;api可实现众多功能&#xff1b; 扫码登录、可对PYQ进行发布、查看、互动点赞、评论&#xff1b; 好友列表、微信消息收发、发文本消息、图片消息、名片消息、动图表情、发文件、删好友&#xff1b; 添加好友、微信转账接收、微…

【Spark入门】基础入门

【大家好&#xff0c;我是爱干饭的猿&#xff0c;本文重点介绍Spark的定义、发展、扩展阅读&#xff1a;Spark VS Hadoop、四大特点、框架模块、运行模式、架构角色。 后续会继续分享其他重要知识点总结&#xff0c;如果喜欢这篇文章&#xff0c;点个赞&#x1f44d;&#xff…

关于fine-tune “微调”

大模型的 Fine-tune 我们对技术的理解&#xff0c;要比技术本身更加重要。 正如我在《大模型时代的应用创新范式》一文中所说&#xff0c;大模型会成为AI时代的一项基础设施。 作为像水、电一样的基础设施&#xff0c;预训练大模型这样的艰巨任务&#xff0c;只会有少数技术…

美SEC与贝莱德,对比特币现货ETF申购方式产生分歧!

比特币现货ETF的通过时间是市场投资者密切关注的议题。虽然SEC最近推迟了Hashdex、富兰克林邓普顿&#xff08;Franklin Templeton&#xff09;和GlobalX申请的决议时间&#xff0c;但彭博ETF分析师James Seyffart对明年一月通过的机率持乐观态度&#xff0c;认为其通过的机会能…

Jmeter参数化之数据库读取数据

以读取mysql数据库为例 1.下载一个mysql驱动包&#xff0c;最好去mysql官网下载 下载网址&#xff1a;https://dev.mysql.com/downloads/connector/j/ elect Operating Systems&#xff1a;选择Platform independent 然后选择zip包&#xff0c;点击Download。 下载如下&…

【24届校招】c++选手还有机会吗?如何选择更好的出路?

一、今年为什么c选手就业形势如此艰难&#xff1f; 去年c岗位的火热&#xff0c;不少c选手拿到高薪offer&#xff0c;今年转c的人群变多&#xff0c;内卷加剧&#xff0c;高学历大佬多如牛毛&#xff0c;很多比较好的c岗位多人投递&#xff0c;僧多肉少。 从行情来说&#xf…

python循环语句和函数

1.使用for循环打印9*9乘法表 for i in range(1, 10):for j in range(1, i1):print(i, "*", j, "", i*j, end"\t")print()结果&#xff1a; 2.使用while循环打印9*9乘法表 i 1 while i < 10:j 1while j < i1:print(i, "*", j…

量子计算软件平台

目录 1.量子语言 2.量子软件开发工具 3.量子云计算平台 1.量子语言 量子语言是一种基于量子计算机的语言&#xff0c;用于描述和实现量子算法。与经典计算机语言不同&#xff0c;量子语言需要考虑量子力学的特殊规则和算法的量子化。其中&#xff0c;最常用的量子语言是量子程…

计算机组成原理-虚拟存储器

文章目录 虚拟存储系统页式虚拟存储器存储器的层次化结构段式虚拟存储器段页式虚拟存储器 虚拟存储系统 将辅存中程序部分调入内存&#xff0c;程序其他待分待需要再调入内存 页式虚拟存储器 将辅存中的程序分页&#xff0c;将当前用得到的程序的页调入到主存中。 外存块号…

ubuntu22.04 arrch64版在线安装node

脚本 #安装node#下载node、npm国内镜像&#xff08;推荐&#xff09;# 判断是否安装了nodeif type -p node; thenecho "node has been installed."elsemkdir -p /home/zenglg cd /home/zenglgwget https://registry.npmmirror.com/-/binary/node/v10.14.1/node-v10.…

Spatialite获取点线面集合的中心点

在这里插入代码片sql SELECT ST_AsText(ST_Centroid(ST_GeomFromText(GEOMETRYCOLLECTION(LINESTRING(105.400538 26.965642, 105.376419 26.938482, 105.350328 26.911685, 105.329089 26.879879, 105.313625 26.84789, 105.301742 26.813179, 105.292141 26.775107, 105.2858…

Python (十四) OS

程序员的公众号&#xff1a;源1024&#xff0c;获取更多资料&#xff0c;无加密无套路&#xff01; 最近整理了一份大厂面试资料《史上最全大厂面试题》&#xff0c;Springboot、微服务、算法、数据结构、Zookeeper、Mybatis、Dubbo、linux、Kafka、Elasticsearch、数据库等等 …

串口数据包收发的思路和流程-stm32入门

本节主要内容&#xff1a; 如何去规定一个合理的数据包格式如何收发数据包 1. 数据包格式规定/定义 1.1 HEX 数据包定义 固定包长&#xff0c;含包头包尾 可变包长&#xff0c;含包头包尾 首先数据包的作用是把一个个单独的数据给打包起来&#xff0c;方便我们进行多字节…