C++之多态(从0到1的突破)

世间百态,每个人都扮演着不同的角色,都进行着不同的行为。C++更是如此,C++中也会出现有着不同行为的多种形态的出现,那就让我们一起进入C++的多态世界吧!!!

一. 多态的概念

多态,顾名思义,就是多种形态。多态分为编译时多态(静态多态)和运⾏时多态(动态多态)。编译时
多态(静态多态)就是函数重载和函数模板,它们传不同类型的参数就可以调⽤不同的函数,通过参数不同达到多种形态,之所以叫编译时多态,是因为它们实参传给形参的参数匹配是在编译时完成的,我们把编译时⼀般归为静态,运⾏时归为动态。运⾏时多态,就是传不同的对象来完成不同的⾏为,从而达到多种形态。

二. 多态的定义和实现

1.多态的构成条件

多态是⼀个继承关系下的类对象,去调⽤同⼀函数,产⽣了不同的⾏为。

注意: 实现多态还有两个必须重要条件

① 必须是基类的指针或者引⽤调⽤虚函数。

② 被调⽤的函数必须是虚函数,并且完成了虚函数重写/覆盖。

原因:要实现多态,第⼀必须是基类的指针或引⽤,因为只有基类的指针或引⽤才能既指向基类
对象⼜指向派⽣类对象;第⼆派⽣类必须对基类的虚函数完成重写/覆盖,重写或者覆盖了,基类和派
⽣类之间才能有不同的函数,多态的不同形态效果才能达到。

2. 虚函数

类成员函数前⾯加virtual修饰,那么这个成员函数被称为虚函数。注意⾮成员函数不能加virtual修
饰。

示例:

class Person
{
public:virtual void show() { cout << "show()" << endl;}
};virtual void f1()  //报错,非成员函数不能用virtual修饰
{}

3. 虚函数的重写/覆盖

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

注意:在重写基类虚函数时,派⽣类的虚函数在不加virtual关键字时,虽然也可以构成重写但是该种写法不是很规范,不建议这样使⽤。

示例:

#define _CRT_SECURE_NO_WARNINGS  1
#include <iostream>
using namespace std;//多态的概念、定义和实现class Person {      //Person是父类
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }
};class Student : public Person {   //Student是子类
public://虚函数重写virtual void BuyTicket() { cout << "买票-打折" << endl; }
};void Func(Person* ptr)
{//这里的函数BuyTicket()的行为和ptr指向的对象有关ptr->BuyTicket();
}//virtual void f1()  //报错,非成员函数不能用virtual修饰
//{}int main()
{Person ps;Student st;Func(&ps);Func(&st);ps.BuyTicket();st.BuyTicket();return 0;
}

多态示例:

class Animal  //父类
{
public:virtual void talk() const{}
};class Dog : public Animal  //子类
{
public:virtual void talk() const{cout << "汪汪" << endl;}
};class Cat : public Animal  //子类
{
public:virtual void talk() const{cout << "喵喵" << endl;}
};void letsHear(const Animal& animal)
{animal.talk();
}int main()
{Cat cat;Dog dog;letsHear(cat);letsHear(dog);return 0;
}

多态应用:

class A
{
public:virtual void func(int val = 1) { cout << "A->" << val <<endl; }virtual void test() { func(); }  //test()的A* this,把B*对象传给this
};class B : public A
{
public:void func(int val = 0) { cout << "B->" << val << endl; }
};int main(int argc, char* argv[])
{B* p = new B;//A* p1 = new A;p->test();  //输出B->1p->func();  //输出B->0return 0;
}

4. 虚函数重写中的协变

派⽣类在重写基类虚函数时,与基类虚函数的返回值类型不同。即基类虚函数返回基类对象的指针或者引⽤,派⽣类虚函数返回派⽣类对象的指针或者引⽤时,称为协变。协变的实际运用很少。

示例:

class A {};class B : public A {};class Person {
public://基类虚函数返回基类对象的指针或引用virtual A* BuyTicket(){cout << "买票-全价" << endl;return nullptr;}
};class Student : public Person {
public://派生类虚函数返回派生类对象的指针或引用virtual B* BuyTicket(){cout << "买票-打折" << endl;return nullptr;}
};void Func(Person* ptr)
{ptr->BuyTicket();
}int main()
{Person ps;Student st;Func(&ps);Func(&st);return 0;
}

5. 析构函数的重写

当基类的析构函数为虚函数时,派⽣类的析构函数只要定义了,⽆论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派⽣类析构函数名字不同看起来不符合重写的规则,但实际上编译器对这里的析构函数的名称做了特殊处理,编译后析构函数的名称统⼀处理成destructor,所以基类的析构函数加了virtual修饰,派⽣类的析构函数就构成重写。

下⾯的代码我们可以看到,如果~A(),不加virtual,那么delete p2时只调⽤的A的析构函数,没有调⽤
B的析构函数,就会导致内存泄漏问题,因为~B()中有资源需要释放。

示例:

class A
{
public://~A()  //有问题,会导致内存泄漏virtual ~A(){cout << "~A()" << endl;}
};class B : public A {
public:~B(){cout << "~B()->delete:" << _p << endl;delete _p;}
protected:int* _p = new int[10];
};// 只有子类Student的析构函数重写了父类Person的析构函数,下⾯的delete对象调⽤析构函数时才能
//构成多态,才能保证p1和p2指向的对象正确的调⽤析构函数,防止内存泄漏。
int main()
{A* p1 = new A;A* p2 = new B;delete p1;delete p2;return 0;
}

6. override和final关键字

C++11提供了override关键字,可以帮助用户检查虚函数是否重写。如果我们不想让子类重写这个虚函数,那么可以⽤final去修饰基类的虚函数。

示例1:

class Car {
public:virtual void Dirve(){}
};class Benz :public Car {
public://报错,override修饰的成员函数不能重写基类成员//virtual void Drive() override { cout << "Benz-comfort" << endl; }
};int main()
{return 0;
}

示例2:

class Car
{
public:virtual void Drive() final {}
};class Benz :public Car
{
public://报错,基类中被final修饰的成员函数不能被子类重写//virtual void Drive() { cout << "Benz-comfort" << endl; }
};int main()
{return 0;
}

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

在这里插入图片描述

注意: 针对多态,重定义也叫隐藏。

三. 纯虚函数和抽象类

在虚函数的后⾯写上 =0 ,则这个函数就为纯虚函数,纯虚函数可以定义实现但是没有必要,只要声明即可。包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象,如果派⽣类继承后不重写纯虚函数,那么派⽣类也是抽象类。纯虚函数某种程度上强制了派⽣类重写虚函数,因为不重写派生类不能实例化对象。

示例:

class Car   //抽象类
{
public:virtual void Drive() = 0; //纯虚函数
};class Benz :public Car
{
public://若父类是抽象类,则未重写父类纯虚函数的子类也是抽象类virtual void Drive(){cout << "Benz-comfort" << endl;}
};class BMW :public Car
{
public:virtual void Drive(){cout << "BMW-control" << endl;}
};int main()
{//Car car;  //报错,抽象类不能实例化对象Car* pBenz = new Benz;pBenz->Drive();Car* pBMW = new BMW;pBMW->Drive();return 0;
}

四.多态的原理

1. 虚函数表指针

示例:

class Base
{
public:virtual void Func1(){cout << "Func1()" << endl;}
protected:int _b = 1;char _ch = 'x';
};int main()
{Base b;//12byte,每个有虚函数的类对象上都会在前面存一个_vfptr的虚函数表指针,指向一个虚函数表//虚函数表存储该类的所有虚函数地址cout << sizeof(b) << endl;return 0;
}

如下图:

在这里插入图片描述

2. 多态的原理

示例:

class Person {
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }virtual void func1() {}void func2() { }
private:string _name;
};class Student : public Person {
public:virtual void BuyTicket() { cout << "买票-打折" << endl; }virtual void func1() {}void func3() {}
private:string _id;
};class Soldier : public Person {
public:virtual void BuyTicket() { cout << "买票-优先" << endl; }
private:string _codename;
};void Func(Person* ptr)
{//这里的函数BuyTicket()的行为和ptr指向的对象有关ptr->BuyTicket();
}int main()
{//多态也会发⽣在多个派生类之间Person ps;Student st;Soldier sr;Func(&ps);Func(&st);Func(&sr);return 0;
}

通过以上代码和下图我们可以看到,满⾜多态条件后,底层不再是编译时通过调⽤对象来确定函数的地址,⽽是运⾏时到对象指向的虚表中确定对应的虚函数的地址,这样就实现了指针或引⽤指向基类就调⽤基类的虚函数,指向派⽣类就调⽤派⽣类对应的虚函数。第⼀张图,ptr指向的Person对象,调⽤的是Person的虚函数;第⼆张图,ptr指向的Student对象,调⽤的是Student的虚函数。

在这里插入图片描述
3. 动态绑定和静态绑定

① 对不满⾜多态条件的函数调⽤是在编译时绑定,也就是编译时确定所调⽤函数的地址,叫做静态绑定。

② 满⾜多态条件的函数调⽤是在运⾏时绑定,也就是在运⾏时到指针指向对象的虚函数表中找到所调⽤函数的地址,也就做动态绑定。

4. 虚函数表

①基类对象的虚函数表中存放基类所有虚函数的地址。同类型的对象共⽤同⼀张虚表,不同类型的对
象各自有独⽴的虚表,所以基类和派⽣类有各自独立的虚表。

② 派⽣类成员由两部分构成,继承下来的基类成员和自己的成员,⼀般情况下,继承下来的基类中有虚函数表指针,派生类自己就不会再⽣成虚函数表指针。但是这⾥继承下来的基类部分虚函数表指针和基类对象的虚函数表指针不是同⼀个,就像基类对象的成员和派⽣类对象中的基类对象成员一样是独立的。

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

④ 派⽣类的虚函数表中包含三个部分:(1)基类的虚函数地址。(2)派⽣类重写的虚函数地址完成覆盖。(3)派⽣类自己的虚函数地址。

⑤ 虚函数表本质就是⼀个存储虚函数表指针的指针数组。

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

⑦ vs下虚函数表是存储在在代码段(常量区)的。

示例:

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()
{Base b;Derive d;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 b1;Derive d1;Base* p3 = &b1;Derive* p4 = &d1;printf("Person虚表地址:%p\n", *(int*)p3);printf("Student虚表地址:%p\n", *(int*)p4);printf("Base虚函数地址:%p\n", &Base::func1);printf("Base普通函数地址:%p\n", &Base::func5);return 0;
}

如图:

在这里插入图片描述

How time flies!!! 相信看到了这里的老铁一定对C++的多态有了初步甚至说是更深的理解,那各位我们就后会有期了!!!

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

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

相关文章

路由器NAT的类型测定

目前所使用的NAT基本都是NAPT&#xff0c;即多端口的NAT技术&#xff0c;因此本文主要是设计了两种测定路由器NAPT类型的实验。 实验环境 设备 主机A&#xff1a;Windows主机B&#xff1a;Windows路由器 软件 ncWiresharkSocketTools 在局域网内部完成所有测试&#xff0c;完全…

ROS 2系统Callback Group概念笔记

核心概念 Callback Group&#xff08;回调组&#xff09;是一个管理一个或多个回调函数执行规则的容器。它决定了这些回调函数是如何被节点&#xff08;Node&#xff09;的 executor 调度的&#xff0c;特别是当多个回调函数同时就绪时&#xff0c;它们之间是并行执行还是必须串…

Qt——主窗口 mainWindow

主窗口 mainWindow 前面学习的所有代码&#xff0c;都是基于QWidget控件&#xff0c;其更多的是作为别的窗口的部分 现在来学习QMainWindow&#xff0c;即主窗口&#xff0c;其包含以下属性 Window Title&#xff1a;标题栏Menu Bar&#xff1a;菜单栏Tool Bar Area&#xff1a…

无训练神经网络影响下的智能制造

摘要 未训练神经网络&#xff08;Untrained Neural Networks, UNNs&#xff09;作为近年来人工智能领域的新兴范式&#xff0c;正在逐步改变智能制造的发展路径。不同于传统深度学习依赖大规模标注数据与高性能计算资源的模式&#xff0c;UNNs 借助网络结构自身的归纳偏置与初…

微服务自动注册到ShenYu网关配置详解

一、配置逐行详解 shenyu:register:registerType: http # 注册中心类型:使用 HTTP 协议进行注册serverLists: ${shenyu-register-serverLists} # ShenYu Admin 的地址列表props:username: ${shenyu-register-props-username} # 注册认证用户名password: ${shenyu-regi…

时序数据库IoTDB的列式存储引擎

在大数据时代&#xff0c;工业物联网&#xff08;IIoT&#xff09;场景正以前所未有的速度生成着海量的时间序列数据。这些数据通常由成千上万的传感器&#xff08;如温度、压力、转速传感器&#xff09;持续不断采集产生&#xff0c;它们具备鲜明的特点&#xff1a;数据时间属…

JavaScript手录18-ajax:异步请求与项目上线部署

前言&#xff1a;软件开发流程 AJAX&#xff1a;前端与后端的数据交互 前后端协作基础 Web应用的核心是“数据交互”&#xff0c;前端负责展示与交互&#xff0c;后端负责处理逻辑与数据存储&#xff0c;二者通过网络请求协作。 &#xff08;1&#xff09;项目开发流程与岗…

HTB 赛季7靶场 - Enviroment

最近所幸得点小闲&#xff0c;补个档嘞&#xff01;~nmap扫描 nmap -F -A 10.10.11.67dirsearch扫描发现login接口 http://environment.htb/login构造如下payload&#xff0c;让程序报错&#xff0c;其原理在于缺失了rember后会导致报错&#xff0c;从而告诉我们一个新的参数ke…

源码编译部署 LAMP 架构详细步骤说明

源码编译部署 LAMP 架构详细步骤说明 一、环境准备 1. 关闭防火墙和SELinux [roothrz ~]# systemctl stop firewalld [roothrz ~]# systemctl disable firewalld [roothrz ~]# setenforce 02. 配置YUM网络源 [roothrz ~]# curl -o /etc/yum.repos.d/CentOS-Base.repo https://m…

机器学习----PCA降维

一、PCA是什么&#xff1f;主成分分析&#xff08;Principal Component Analysis&#xff0c;PCA&#xff09;是机器学习中最常用的降维技术之一&#xff0c;它通过线性变换将高维数据投影到低维空间&#xff0c;同时保留数据的最重要特征。PCA由卡尔皮尔逊于1901年发明&#x…

ReactNative开发实战——React Native开发环境配置指南

一、开发前准备 1. macOS平台基础工具安装 brew install node18 brew install watchman brew install cocoapods2. 代理配置 npm config set proxy http://127.0.0.1:7890 npm config set https-proxy http://127.0.0.1:7890# 新增扩展建议&#xff08;可选配置&#xff09; ec…

差速转向机器人研发:创新驱动的未来移动技术探索

在科技日新月异的今天&#xff0c;机器人技术作为智能制造与自动化领域的核心驱动力&#xff0c;正以前所未有的速度发展。其中&#xff0c;差速转向机器人以其独特的运动机制和广泛的应用前景&#xff0c;成为了科研与工业界关注的焦点。本文旨在探讨差速转向机器人研发进展&a…

Wireshark捕获电脑与路由器通信数据,绘制波形观察

一、准备工作 电脑发出数据的波形图绘制在我的另一篇博客有详细介绍&#xff1a; 根据Wireshark捕获数据包时间和长度绘制电脑发射信号波形-CSDN博客 路由器发送给电脑数据的波形图绘制也在我的另一篇博客有详细介绍&#xff1a; 根据Wireshark捕获数据包时间和长度绘制路由…

汽车ECU实现数据安全存储(机密性保护)的一种方案

一、 综述在车辆ECU中总是有一些密钥或重要数据需进行机密性保护&#xff0c;但因产品选型、成本等考虑&#xff0c;导致一些ECU的芯片不支持硬件安全模块&#xff08;例如HSM、TEE等&#xff09;。此时&#xff0c;为保障数据的机密性&#xff0c;可考虑通过软件实现数据的安全…

AI 效应: GPT-6,“用户真正想要的是记忆”

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…

云计算学习100天-第25天

部署LNMP环境安装软件#在前一天已经安装nginx的基础上安装MariaDB&#xff0c;php和php-fpm yum -y install mariadb mariadb-server mariadb-devel php php-mysqlnd php-fpm #mariadb&#xff08;数据库客户端软件&#xff09;、mariadb-server&#xff08;数据库服务器软件&…

细化的 Spring Boot 和 Spring Framework 版本对应关系

注:本文由ai辅助,个人整理,有问题可留言 Spring Boot 3.x 系列 (基于 Spring Framework 6.x) Spring Boot 版本 对应的 Spring Framework 版本 Java 支持版本 3.1.5 (最新) 6.0.15 Java 17+ 3.1.4 6.0.14 Java 17+ 3.1.3 6.0.12 Java 17+ 3.1.2 6.0.11 Java 17+ 3.1.1 6.0.…

PyTorch API 1

文章目录torch张量创建操作索引、切片、连接与变异操作加速器生成器随机采样原地随机采样准随机采样序列化并行计算局部禁用梯度计算数学运算常量逐点运算归约操作比较运算频谱操作其他操作BLAS 和 LAPACK 运算遍历操作实用工具符号数字导出路径控制流优化方法操作符标签torch.…

基于FPGA的实时图像处理系统(2)——VGA显示彩条和图片

VGA显示彩条和图片 文章目录VGA显示彩条和图片一、VGA简介二、功能设计1、彩条设计2、图片设计三、结果展示四、代码一、VGA简介 VGA(Video Graphics Array)是IBM在1987年随PS/2机⼀起推出的⼀种视频&#xff0c;具有分辨率⾼、显⽰速率快、颜⾊丰富等优点&#xff0c;在彩 ⾊…

【网络运维】Linux 文本处理利器:sed 命令

Linux 文本处理利器&#xff1a;sed 命令 sed 简介 sed&#xff08;Stream Editor&#xff09;是一款非交互式的流编辑器&#xff0c;诞生于 1973–1974 年间的贝尔实验室&#xff0c;由 McMahon 开发。它专为文本处理而生&#xff0c;功能强大&#xff0c;是 Linux 文本处理常…