C++多态(详解)

一、多态的概念

1.1、多态的概念

多态:多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。

举个例子:比如买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人
买票时是优先买票。

1.2、多态的分类

多态分为静态多态和动态多态

1)静态多态,也称为静态绑定或前期绑定(早绑定):函数重载和函数模板实例化出多个函数(本质也是函数重载)。静态多态也称为编译期间的多态,编译器在编译期间完成的,编译器根据函数实参的类型(可能会进行隐式类型转换),可推断出要调用那个函数,如果有对应的函数就调用该函数,否则出现编译错误。

2)动态多态,也称为动态绑定或后期绑定(晚绑定):在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,即运行时的多态。在程序执行期间(非编译期)判断所引用对象的实际类型,根据其实际类型调用相应的方法。

  1. 父类指针或引用指向父类,调用的就是父类的虚函数
  2. 父类指针或引用指向子类,调用的就是子类的虚函数

二、多态的定义及实现

2.1、多态的构成条件

多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。比如Student继承了
Person。Person对象买票全价,Student对象买票半价。

在继承中要构成多态还有两个条件:

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

2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

class Person //成人
{public:virtual void fun(){cout << "全价票" << endl; //成人票全价}
};
class Student : public Person //学生
{public:virtual void fun() //子类完成对父类虚函数的重写{cout << "半价票" << endl;//学生票半价}
};
void BuyTicket(Person* p)
{p->fun();
}int main()
{Student st;Person p;BuyTicket(&st);//子类对象切片过去BuyTicket(&p);//父类对象传地址
}

2.2、虚函数

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

class Person {
public:virtual void BuyTicket() { cout << "买票-全价" << endl;}
};

2.3、虚函数的重写

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

class Person {
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:virtual void BuyTicket() {cout << "买票-半价" << endl; }/*注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因
为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议
这样使用*//*void BuyTicket() { cout << "买票-半价" << endl; }*/};
void Func(Person& p)
{ p.BuyTicket(); 
}
int main()
{Person ps;Student st;Func(ps);Func(st);return 0;
}

虚函数重谢的两个特例:

1、协变(基类与派生类虚函数返回值类型不同)

派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指
针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变.

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

2、析构函数的重写(基类与派生类析构函数的名字不同)

如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,
都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,
看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处
理,编译后析构函数的名称统一处理成destructor。

class Person {
public:virtual ~Person() {cout << "~Person()" << endl;}
};
class Student : public Person {
public:virtual ~Student() { cout << "~Student()" << endl; }
};
// 只有派生类Student的析构函数重写了Person的析构函数,下面的delete对象调用析构函
数,才能构成多态,才能保证p1和p2指向的对象正确的调用析构函数。
int main()
{Person* p1 = new Person;Person* p2 = new Student;delete p1;delete p2;return 0;
}

2.4、override 和 final

 final:修饰虚函数,表示该虚函数不能再被重写

class Car
{
public:virtual void Drive() final {}
};
class Benz :public Car
{
public:virtual void Drive() {cout << "Benz-舒适" << endl;}
};

override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。

class Car{
public:virtual void Drive(){}
};
class Benz :public Car {
public:virtual void Drive() override {cout << "Benz-舒适" << endl;}
};

2.5、重载、覆盖(重写)、隐藏(重定义)的·对比


三、抽象类

3.1、抽象类概念

在虚函数的后面写上 =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;}
};
void Test()
{Car* pBenz = new Benz;pBenz->Drive();Car* pBMW = new BMW;pBMW->Drive();
}

3.2、接口继承和实现继承

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实
现。虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成
多态,继承的是接口。所以如果不实现多态,不要把函数定义成虚函数。

四、多态的实现原理

「多态」的关键在于通过基类指针或引用调用一个虚函数时,编译时不能确定到底调用的是基类还是派生类的函数,运行时才能确定。

4.1、虚函数表

class A
{
public:int i;virtual void Print() { } // 虚函数
};class B
{
public:int n;void Print() { }
};int main()
{A a;B b;cout << sizeof(a) << "," << sizeof(b);return 0;
}

A对象当中除了i成员外,实际上还有一个指针_vfptr放在对象的前面(有些平台可能会放到对象的最后面,这个跟平台有关)。

对象中的这个指针叫做虚函数表指针,简称虚表指针,虚表指针指向一个虚函数表,简称虚表,每一个含有虚函数的类中都至少有一个虚表指针。

虚表当中存储的就是虚函数的地址,因为父类当中的Func1和Func2都是虚函数,所以父类对象b的虚表当中存储的就是虚函数Func1和Func2的地址。

通过上面的知识铺垫,发现一下几个问题:

1. 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员。

2. 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表
中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数
的覆盖。重写是语法的叫法,覆盖是原理层的叫法。


3. 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函
数,所以不会放进虚表。


4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。


5. 总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。


6. 虚函数存在哪的?虚表存在哪的?

虚函数存在虚表,虚表存在对象中。注意上面的回答的错的。注意虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。

1、虚表指针在构造函数阶段(初始化列表)填入到对象中,虚表则是在编译时就生成好了。
2、虚表里面放的是虚函数地址,虚函数和普通函数一样,编译完成以后,都是放在代码段中。
3、一个类中所有的虚函数,都会放在虚表中。
4、子类会将父类的虚表拷贝一份,然后用重写的虚函数地址覆盖掉原来虚表中的函数地址,因此虚函数的重写,也叫虚函数的覆盖。
 

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

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

相关文章

100. 相同的树(Java)

目录 解法&#xff1a; 官方解法&#xff1a; 方法一&#xff1a;深度优先搜索 复杂度分析 时间复杂度&#xff1a; 空间复杂度&#xff1a; 方法二&#xff1a;广度优先搜索 复杂度分析 时间复杂度&#xff1a; 空间复杂度&#xff1a; 给你两棵二叉树的根节点 p 和…

【日常总结】mybatis-plus WHERE BINARY 中文查不出来

目录 一、场景 二、问题 三、原因 四、解决方案 五、拓展&#xff08;全表全字段修改字符集一键更改&#xff09; 准备工作&#xff1a;做好整个库备份 1. 全表一键修改 Stage 1&#xff1a;运行如下查询 Stage 2&#xff1a;复制sql语句 Stage 3&#xff1a;执行即可…

了解Linux网络配置

本章主要介绍网络配置的方法。 网络基础知识 查看网络信息 图形化界面修改 通过配置文件修改 命令行管理 11.1 网络基础知识 一台主机需要配置必要的网络信息&#xff0c;才可以连接到互联网。需要的配置网络信息包括IP、 子网掩码、网关和 DNS。 11.1.1 IP 地址 在计算机…

一文3000字从0到1用Python进行gRPC接口测试!

gRPC 是一个高性能、通用的开源RPC框架&#xff0c;其由 Google 主要面向移动应用开发并基于HTTP/2 协议标准而设计&#xff0c;基于 ProtoBuf(Protocol Buffers) 序列化协议开发&#xff0c;且支持众多开发语言。 自gRPC推出以来&#xff0c;已经广泛应用于各种服务之中。在测…

Python 作业答疑

1. 哥德巴赫猜想 问题描述&#xff1a; 2000以内的不小于4的正偶数都能够分解为两个素数之和&#xff08;即验证歌德巴赫猜想对2000以内的正偶数成立&#xff09;。 问题分析&#xff1a; 根据问题描述&#xff0c;为了验证歌德巴赫猜想对2000以内的正偶数都是成立的&#…

Android 11 适配——整理总结篇

背景 > 经过检测&#xff0c;我们识别到您的应用&#xff0c;目前未适配安卓11&#xff08;API30&#xff09;&#xff0c;请您关注适配截止时间&#xff0c;尽快开展适配工作&#xff0c;避免影响应用正常发布和经营。 > targetSdkVersion30 升级适配工作参考文档&am…

SRC挖掘漏洞XSS

Markdown是一种轻量级标记语言&#xff0c;创始人为约翰格鲁伯&#xff08;John Gruber&#xff09;。它允许人们使用易读易写的纯文本格式编写文档&#xff0c;然后转换成有效的 XHTML&#xff08;或者HTML&#xff09;文档。这种语言吸收了很多在电子邮件中已有的纯文本标记的…

Hadoop学习笔记(HDP)-Part.20 安装Flume

目录 Part.01 关于HDP Part.02 核心组件原理 Part.03 资源规划 Part.04 基础环境配置 Part.05 Yum源配置 Part.06 安装OracleJDK Part.07 安装MySQL Part.08 部署Ambari集群 Part.09 安装OpenLDAP Part.10 创建集群 Part.11 安装Kerberos Part.12 安装HDFS Part.13 安装Ranger …

鸿蒙开发ArkTS语言—状态管理概述

概述 我们构建的页面多为静态界面。如果希望构建一个动态的、有交互的界面&#xff0c;就需要引入“状态”的概念。 图1 效果图 上面的示例中&#xff0c;用户与应用程序的交互触发了文本状态变更&#xff0c;状态变更引起了UI渲染&#xff0c;UI从“Hello World”变更为“Hel…

方法-TensorBoard如何在PC端远程可视化服务器的训练结果

本专栏为深度学习的一些技巧,方法和实验测试,偏向于实际应用,后续不断更新,感兴趣童鞋可关,方便后续推送 简介 TensorBoard 是 TensorFlow 的可视化工具&#xff0c;可以帮助用户实时监控深度神经网络的训练过程,基于PyTorch的训练过程也可以可视化。假设我在服务器上进行模型…

SpringSecurity6 | 修改默认用户

✅作者简介:大家好,我是Leo,热爱Java后端开发者,一个想要与大家共同进步的男人😉😉 🍎个人主页:Leo的博客 💞当前专栏: Java从入门到精通 ✨特色专栏: MySQL学习 🥭本文内容:SpringSecurity6 | 修改默认用户 📚个人知识库 :知识库,欢迎大家访问 学习参考…

【前端架构】清洁前端架构

探索前端架构&#xff1a;概述与干净的前端架构相关的一些原则&#xff08;SOLID、KISS、DRY、DDD等&#xff09;。 在我之前的一篇帖子中&#xff0c;我谈到了Signals和仍然缺少的内容[1]。现在&#xff0c;我想谈谈一个更通用的主题&#xff0c;即Clean Frontend Architectu…