【C++】——多态

一.多态的概念

1.多态

多态(polymorphism)的概念:通俗的来说,就是多种形态。多态分为静态多态(编译时多态)和动态多态(运行时多态),而我们讲的多态大部分都是动态多态。

静态多态主要就是我们前面了解过的函数模板和函数重载,它们传不同的参数就可以调用不同的函数,通过参数不同达到多种形态,这所以叫编译时多态,是因为实参传递给实参的参数匹配过程是在编译时完成的,我们一般把编译时归为静态,运行时归为动态。

运行时多态,简单来说,就是对于同一个函数,不同的对象去调用的会完成不同的行为,以此来达到多种形态。比如买火车票这个行为,普通人买票就是全价;学生买票就买学生票(有优惠);军人买票可以优先买票。

这里先演示一下多态的运行结果:

class person
{
public:virtual void buyticket(){cout << "全价" << endl;}
};class student : public person
{
public:virtual void buyticket(){cout << "半价" << endl;}
};void func(person* p)
{p->buyticket();
}int main()
{person p;student s;func(&p);func(&s);return 0;
}

对这段程序来说,person和student都有买票这个行为,但是不同的对象行为是不同的,我们用一个基类的指针去调用buyticket这个函数,其运行结果会根据对象的不同而不同,当基类指针接收的是一个基类对象的地址时,调用的就是基类的buyticket;当基类指针接受的是一个派生类的对象的地址时,调用的就是派生类的buyticket。


 2.虚函数

类的成员函数前面加上virtual来修饰,那么这个成员就被称为虚函数。注意:非成员函数不能被virtual修饰。

class A
{virtual bool max(int a, int b){return a > b;}protected:int _a;int _b;
};

当该类作为基类被派生类继承后,在派生类中该虚函数依旧还是虚函数。

3.多态实现的前提

多态是在继承体系中,基类对象和派生类对象去调用同一函数,产生了不同的行为。

要实现多态的效果必须满足下面的两个条件:

1、必须是基类的指针或者引用调用该成员函数:因为基类的指针或者引用既可以表示基类对象也可以表示派生类对象。

2、被调用的成员函数必须是虚函数,且在派生类中已经完成了重写/覆盖。

3.1虚函数的重写/覆盖

派生类中有一个与基类三同(虚函数返回值、函数名、形参列表完全相同,ps:形参列表相同只要求形参的类型相同,不要求形参名相同)的虚函数即派生类的虚函数重写了基类的虚函数。

注意:在重写基类的虚函数时,派生类的虚函数加不加virtual都可以(因为继承后基类的虚函数被派生类继承下来了,该函数在派生类中依旧保持虚函数属性),但是这样写并不规范,建议在重写时也加上virtual。


我们现在会看前面给出的那段程序,首先虚函数在派生类中完成了重写,且在调用该函数时是利用基类的指针调用的(引用也可以),不同的是基类的指针指向的对象不同,第一次指向基类对象,第二次指向派生类对象。 


多态面试题1

以下程序输出的结果是什么?

A.A->0        B.B->1        C.A->1        D.B->0        E.编译出错        F.以上答案都不对

class A
{
public:virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }virtual void test() { func(); }
};class B : public A
{
public:void func(int val = 0) { std::cout << "B->" << val << std::endl; }
};int main(int argc, char* argv[])
{B* p = new B;p->test();return 0;
}

答案是:B

首先创建了一个B类对象的指针,然后调用了test函数,B类对象的指针之所以可以调用test函数,是因为B从A继承了它;然后在test函数中又调用了func函数,要注意的是,这里调用func函数时其实满足了多态的条件,因为在成员函数中,调用其他成员函数时默认有一个this指针。这个this指针就是基类对象的指针,然后func完成了重写,且这里的this指向的是一个B类对象,所以这里应该调用B类的func函数,所以应该打印B->。

那按道理来说应该是B->0,为什么是B->1呢?

注意,重写的本质是重写虚函数的函数体,所以在我们调用重写后的虚函数时,其返回值、函数名、形参表都与基类的相同,不同的只有函数体。所以这里本质上val还是1.

而当我们直接调用B类中的func时,此时与多态无关,val用的就是0.

注意:不要修改重写后的函数缺省值。


4.虚函数重写——协变 

先前说,虚函数重写时要求三同。

但是其返回值可以不同,但要求返回值是具有父子关系的类型指针/引用,这个规则称为协变。

class A{};
class B:public A{};class person
{
public://virtual person* buyticket()virtual A* buyticket(){cout << "成人票" << endl;return nullptr;}
};class student : public person
{
public://virtual student* buyticket()virtual B* buyticket(){cout << "学生票" << endl;return nullptr;}
};

我们既可以直接用person/student类的指针作为返回值,也可以用另一组具有父子关系的类作为返回值。这也是一种协变。协变也满足多态的条件,也可以实现多态。

5.析构函数的重写

基类的析构函数为虚函数,只要派生类定义了析构函数,无论是否加virtual都与基类的析构函数构成重写。虽然这两个析构函数的名字不同,但其实在编译阶段,所有的析构函数的函数名都被处理成了destructor,所以也满足了三同。

所以,只要基类的析构函数是虚函数,派生类只要显式定义析构函数,就构成了重写。


面试题2

为什么基类中的析构函数建议设计成虚函数?

为了避免基类指针指向派生类对象时,调用析构函数不能完全清理派生类对象的全部数据,造成的内存泄露问题。

我们可以借助下面这段程序来理解:

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

当我们分别用基类的指针指向了基类对象和派生类对象时,析构pa时,就是很普通的析构,但是当析构pb时,此时满足了多态,会先调用B的析构函数,然后再调用A的析构函数(派生类析构函数调用结束时会自动调用基类的析构函数),就可以将pb指向的内容全部销毁。

但是如果没有将A类的析构函数定义为虚函数的话,此时析构pb就不会产生多态效果,直接调用了A的析构,但是pb指向的是B类对象,等于它只析构了B类对象中的A类部分,还有B类自己的部分没有析构,这就导致了内存泄漏。


6.override、final

override是C++11中新增的一个关键字,用来检测派生类中的指定函数是否完成了重写。也就是说,用override修饰的函数,必须是基类虚函数的重写,否则就会报错。

我们用override修饰了派生类的这个函数,编译阶段,编译器就会向上搜索,判断这个函数是否为基类的虚函数的重写,如果是,不报错;不是,就报错。

我们看到,这里的函数名写错了,所以不构成重写。

所以,我们可以利用这个关键字来替我们检查是否完成了重写。 

如果我们不想这个虚函数被派生类重写,那么我们可以用final来修饰它。 

7.重载/重写/隐藏的对比 

这是三个研究的都是同名函数之间的关系。 

8.纯虚函数和抽象类

当一个虚函数以=0为结尾时,这个虚函数就是纯虚函数

class A
{
public:virtual void func() = 0{}
};

纯虚函数可以有函数体,也可以没有。

包含纯虚函数的类,叫做抽象类。抽象类不能用来定义对象,但是可以作为基类

如果继承该抽象类的派生类没有重写该纯虚函数的话,该派生类也是一个抽象类。 


二.多态的原理 

1.虚函数表指针

下面这段程序在32位下运行结果是什么?

A.编译报错        B.运行报错        C.8        D.12

class Base
{
public:virtual void Func1(){cout << "Func1()" << endl;}
protected:int _b = 1;char _ch = 'x';
};
int main()
{Base b;cout << sizeof(b) << endl;return 0;
}

答案是:D

答案不是8而是12的原因是,在Base类对象的头部还有一个虚函数表指针__vfptr(注意有些平台可能会放到对象的最后面,这个跟平台有关),这个指针指向一个函数指针数组,该函数指针数组里面存储的都是Base类的虚函数的指针。而在32位下,指针的大小是4字节,加上int和char,在进行内存对齐的话,刚好是12字节。

一个含有虚函数的类中至少都有一个虚函数表指针,因为一个类所有的虚函数的地址都要被放到这个类对象的虚函数表中,虚函数表也简称为虚表。

 同一个类的不同对象公用同一个虚表

当派生类没有重写该虚函数时,此时派生类和基类的虚表指针的内容是一样的,但是虚函数表指针不同。

重写后,派生类就会将之前的那个地址给覆盖掉

2.多态的原理 

针对下面的程序,在func中当p指向的是person类的对象时就调用的是person的buyticket,p指向的是student类的对象时就调用student的buyticket,这是为什么呢?

我们在前面说了,一个有虚函数的类的对象都会有一个虚函数表指针,里面存放着该类所有虚函数的地址。在满足多态的前提下,在运行时,当p指向的是person对象时,p就会到person类的虚表里面去找对应虚函数的地址,然后去调用;当p指向的是student对象时,p就会到student类的虚表里面去找对应虚函数的地址,然后调用。

class person
{
public:virtual void buyticket(){cout << "person" << endl;}protected:string _name;int _age;
};class student : public person
{
public:virtual void buyticket() override{cout << "student" << endl;}protected:int _id;
};void func(person* p)
{p->buyticket();
}int main()
{person p;student s;func(&p);func(&s);return 0;
}

 

3.动态绑定和静态绑定 

对不满足多态条件(指针/引用+调用虚函数)的函数调用是在编译时绑定的,也就是在编译时确定调用函数的地址,叫做静态绑定

满足多态条件的函数调用是在运行时绑定的,也就是在运行时到指定对象的虚表中找到调用函数的地址,叫做动态绑定。

 4.虚函数表

1、基类对象的虚函数表中存放基类所有的虚函数的地址。非虚函数的地址是不会存在里面的。

 2、派生类由两部分构成,继承下来的基类和自己的成员,一般情况下,继承下来的基类中有虚函数表指针,自己就不会再生成虚函数表指针。但是要注意的这里继承下来的基类部分虚函数表指针和基类对象的虚函数表指针不是同一个,它们只是里面存储的地址相同。这就像基类对象和派生类对象中的基类部分。

这里调式窗口看不到B类自己的func4虚函数,可以借助内存窗口观察 

3、派⽣类中重写的基类的虚函数,派⽣类的虚函数表中对应的虚函数就会被覆盖成派⽣类重写的虚函 数地址 

4、派⽣类的虚函数表中包含,基类的虚函数地址,派⽣类重写的虚函数地址,派⽣类⾃⼰的虚函数地 址三个部分。

虚函数表本质是⼀个存虚函数指针的指针数组,⼀般情况这个数组最后⾯放了⼀个0x00000000标 记。(这个C++并没有进⾏规定,各个编译器⾃⾏定义的,vs系列编译器会再后⾯放个0x00000000 标记,g++系列编译不会放)

5、虚函数存在哪的?

虚函数和普通函数⼀样的,编译好后是⼀段指令,都是存在代码段的,只是虚函数的地址⼜存到了虚表中 

6、虚函数表存在哪里?

 为了确认,我们可以写一段程序,用来判断其在那块空间上存储

我们可以将其和每个空间上的变量的地址进行比较,如果存储在同一个空间的话,地址应该比较接近,为了获取虚表指针存储的地址,因为该指针在对象的头部,其大小是四个字节,所以我们可以先将其强转成int*,然后在对其进行解引用,只拿到前四个字节的内容,就可以拿到里面的地址。

class Base {
public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }void func5() { cout << "Base::func5" << endl; }
protected:int a = 1;
};class Derive : public Base
{
public:// 重写基类的func1virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func1" << endl; }void func4() { cout << "Derive::func4" << endl; }protected:int b = 2;
};int main()
{int i = 0;static int j = 1;int* p1 = new int;const char* p2 = "xxxxxxxx";printf("栈:%p\n", &i);printf("静态区:%p\n", &j);printf("堆:%p\n", p1);printf("常量区:%p\n", p2);Base b;Derive d;Base* p3 = &b;Derive* p4 = &d;printf("Person虚表地址:%p\n", *(int*)p3);printf("Student虚表地址:%p\n", *(int*)p4);printf("虚函数地址:%p\n", &Base::func1);printf("普通函数地址:%p\n", &Base::func5);return 0;
}

我们对比可以发现,虚表地址和常量区的地址接近,所以可以认为,虚表就存储在常量区中。 


多态部分的细节:

1、被virtual修饰的成员函数称为虚函数 

2、virtual关键字只在声明时加上,在类外实现时不能加

3、static和virtual是不能同时使用的

4、实现多态是要付出代价的,如虚表,虚表指针等,所以不实现多态就不要有虚函数了

5、抽象类可以定义指针,而且经常这样做,其目的就是用父类指针指向子类从而实现多态

6、基类有几张虚表,派生类就有几张,派生类自己的虚函数不会开一个新的虚表存储自己虚函数的地址,其地址会存放到第一张虚表的末尾。


下面程序的运行结果:

答案 0 1 2;

new B时,会调用B类的构造函数,但是调用之前会先调用基类的构造函数,然后执行test(),此时多态还没有形成,所以调用的就是A类的func(),打印0;

然后到B类的构造函数,执行test(),由于基类已经创建成功,虚表已经存在了,所以此时构成了多态,调用B类的func,打印1;

最后执行p->test();满足多态,++后打印,2;

class A
{
public:A() :m_iVal(0) { test(); }virtual void func() { std::cout << m_iVal << ' '; }void test() { func(); }
public:int m_iVal;
};class B : public A
{
public:B() { test(); }virtual void func(){++m_iVal;std::cout << m_iVal << ' ';}
};int main(int argc, char* argv[])
{A* p = new B;p->test();return 0;
}

完~

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

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

相关文章

Linux基础4-进程4(环境变量,命令行参数详解)

上篇文章:Linux基础4-进程3(进程优先级&#xff0c;竞争&#xff0c;独立&#xff0c;并行&#xff0c;并发&#xff0c;进程切换)-CSDN博客 本章重点: Linux中环境变量的理解和使用 目录 一. 环境变量概念和查看环境变量 1.1 环境变量概念 1.2 查看环境变量 二. 获取环境变…

【复平面】-复数相乘的几何性质

文章目录 从数学上证明1. 计算乘积 z 1 ⋅ z 2 z_1 \cdot z_2 z1​⋅z2​2. 应用三角恒等式3. 得出结果 从几何角度证明1.给出待乘的复数 u i u_i ui​2.给出任意复数 l l l3.复数 l l l 在不同坐标轴下的表示图 首先说结论&#xff1a; 在复平面中&#xff0c;两个复数&a…

如何将现有VUE项目所有包更新到最新稳定版

更新有风险,Enter要谨慎!!! 要将项目中的所有 npm 包更新到最新稳定版&#xff0c;可以使用 npm-check-updates 工具。以下是具体步骤&#xff1a; 步骤一&#xff1a;安装 npm-check-updates 首先&#xff0c;全局安装 npm-check-updates 工具&#xff1a; npm install -g…

excel常用技能

1.基础技能 1.1 下拉框设置 a. 选中需要设置的列或单元格&#xff0c;数据 ---》 数据验证 b.验证条件 ---> 序列&#xff08;多个值逗号隔开&#xff09; 2.函数 2.1 统计函数-count a.count(区域&#xff0c;区域&#xff0c;......) 统计数量&#xff0c;只针…

(linux驱动学习 - 12). IIC 驱动实验

目录 一.IIC 总线驱动相关结构体与函数 1.i2c_adapter 结构体 2.i2c_algorithm 结构体 3.向系统注册设置好的 i2c_adapter 结构体 - i2c_add_adapter 4.向系统注册设置好的 i2c_adapter 结构体 - i2c_add_numbered_adapter 5.删除 I2C 适配器 - i2c_del_adapter 二.IIC 设…

华为ensp防火墙配置(纯享版)

文章目录 前言一、拓扑结构二、配置步骤1.路由器配置&#xff08;路由器代替互联网&#xff09;2.server和pc配置3.防护墙配置4.测试 总结 前言 防火墙是生活和项目中不可或缺的一部分&#xff0c;本篇文章对华为的ensp防火墙配置做一个总结。在之前的dhcp配置中有软件的下载地…

996引擎 - 活捉NPC

996引擎 - 活捉NPC 引擎触发 - 引擎事件(QF)事件处理模块 GameEvent测试文件参考资料 引擎触发 - 引擎事件(QF) cfg_game_data 配置 ShareNpc1 可以将QM和机器人的触发事件全部转到 QF 引擎触发是通用的,TXT的所有触发转换成小写后在LUA中就可使用,如说明书中缺省可反馈至对接群…

如何借助AI 来提高开发效率

前言 随着人工智能&#xff08;AI&#xff09;技术的快速发展&#xff0c;特别是大规模语言模型&#xff08;如 GPT 系列&#xff09;的崛起&#xff0c;软件开发领域正在经历一场革命。AI 大模型不仅在代码生成方面展现出强大的能力&#xff0c;还在测试、维护和创新等多个环…

QML项目实战:自定义Button

目录 一.添加模块 ​1.QtQuick.Controls 2.1 2.QtGraphicalEffects 1.12 二.自定义Button 1.颜色背景设置 2.设置渐变色背景 3.文本设置 4.点击设置 5.阴影设置 三.效果 1.当enabled为true 2.按钮被点击时 3.当enabled为false 四.代码 一.添加模块 1.QtQuick.Con…

HarmonyOS NEXT应用元服务开发Intents Kit(意图框架服务)本地搜索接入方案

一、方案概述 当用户使用应用/元服务时&#xff0c;开发者可以按照标准意图Schema向系统共享数据&#xff0c;并支持意图调用&#xff08;空调用与传参调用&#xff09;&#xff0c;以实现用户点击卡片后&#xff0c;可后台执行功能&#xff08;例如播放指定歌曲&#xff09;或…

CyclicBarrier使用详解及遇到的坑

上一篇文章讲的是关于是使用CountDownLatch实现生成年底报告遇到的问题&#xff0c;这个计数器和CyclicBarrier也有类似功能&#xff0c;但是应用场景不同。 一、应用场景 CountDownLatch&#xff1a; 有ABCD四个任务&#xff0c;ABC是并行执行,等ABC三个任务都执行完…

k8s-service、endpoints、pod之间是怎么进行网络互通的

k8s-service、endpoints、pod之间是怎么进行网络互通的 1、service2、endpoints3、service、endpoints、pod通信图4、不通服务pod内部间访问 1、service 在K8S中&#xff0c;Service是一种抽象&#xff0c;定义了一组Pod的逻辑集合和访问这些Pod的策略。首先&#xff0c;我们需…

资产管理系统:SpringBoot技术实现

企业资产管理系统 摘要 随着信息技术在管理上越来越深入而广泛的应用&#xff0c;管理信息系统的实施在技术上已逐步成熟。本文介绍了企业资产管理系统的开发全过程。通过分析企业资产管理系统方面的不足&#xff0c;创建了一个计算机管理企业资产管理系统的方案。文章介绍了企…

I.MX6U 裸机开发5.准备C环境并用C语言控制LED

I.MX6U 裸机开发5.准备C环境并用C语言控制LED 一、C运行环境1. 设置处理器模式2. CPSR 寄存器CPSR 寄存器结构模式位MRS 指令MSR 指令 3. 设置SP指针设置 SP 指针示例 保存和恢复 SP 指针示例 4. 跳转到C语言 二、程序编写1. 启动文件 start.S2. main.h 定义寄存器3. 主程序mai…

c++设计模式demo

模式设计原则 依赖倒置原则 ⾼层模块不应该依赖低层模块&#xff0c;⼆者都应该依赖抽象 &#xff1b; 抽象不应该依赖具体实现&#xff0c;具体实现应该依赖于抽象&#xff1b; ⾃动驾驶系统公司是⾼层&#xff0c;汽⻋⽣产⼚商为低层&#xff0c;它们不应该互相依赖&#x…

【go从零单排】泛型(Generics)、链表

&#x1f308;Don’t worry , just coding! 内耗与overthinking只会削弱你的精力&#xff0c;虚度你的光阴&#xff0c;每天迈出一小步&#xff0c;回头时发现已经走了很远。 &#x1f4d7;概念 在Go语言中&#xff0c;泛型&#xff08;Generics&#xff09;允许你编写可以处理…

Web前端开发--HTML语言

文章目录 前言1.介绍2.组成3.基本框架4.常见标签4.1双标签4.1.1.标题标签4.2.2段落标签4.1.3文本格式化标签4.1.4超链接标签4.1.5视频标签4.1.6 音频标签 4.2单标签4.2.1换行标签和水平线标签4.2.2 图像标签 5.表单控件结语 前言 生活中处处都有网站&#xff0c;无论你是学习爬…

数据结构-图的概念

不存在空图现象,顶点集不能为空,边集可以为空 研究链接一个顶点的边有多少条非常有意义 无向图的度边的二倍 有向图的入度出度,度边数 有向图一致 重点 子图必须联通,尽可能多的边和结点 对于一个生成树,他有n个节点就有n-1条边 修路问题将各个村庄相连,由于经费有限,只能选择…

TDengine 签约蘑菇物联,改造通用设备工业互联网平台

在当前工业互联网迅猛发展的背景下&#xff0c;企业面临着日益增长的数据处理需求和智能化转型的挑战。通用工业设备的高能耗问题愈发突出&#xff0c;尤其是由这些设备组成的公辅能源车间&#xff0c;亟需更高效的解决方案来提升设备运行效率&#xff0c;降低能源消耗。为此&a…

LSM-TREE和SSTable

一、什么是LSM-TREE LSM Tree 是一种高效的写优化数据结构&#xff0c;专门用于处理大量写入操作 在一些写多读少的场景&#xff0c;为了加快写磁盘的速度&#xff0c;提出使用日志文件追加顺序写&#xff0c;加快写的速度&#xff0c;减少随机读写。但是日志文件只能遍历查询…