从零开始的c++之旅——多态

1. 多态的概念

        通俗来说就是多种形态。

        多态分为编译时多态(静态多态)和运行时多态(动态多态)。

        编译时多态主要就是我们之前提过的函数重载和函数模板,同名提高传不同的参数就可以调
        用不同的函数,通过参数不同达到多种形态,由于他们实参传递给形参匹配是在编译时完}
        成,我们把编译时⼀般归为静态,运⾏时归为动态。

        运行时多态,就是指完成某个行为,通过传不同的参数可以产生不同的行为,达到多种形
        态。比如买票,普通人全价购买,学生则可以搬家,军人则是优先买票。

2. 多态的定义及实现

2.1 多态的构成条件

        多态就是一个继承关系的下的类对象,去调⽤同⼀函数,产⽣了不同的⾏为。⽐如Student继承了 Person。Person对象买票全价,Student对象优惠买票。

2.1.1 实现多态还有两个必须的条件:

        • 必须指针或者引⽤调⽤虚函数

        • 被调⽤的函数必须是虚函数。

要想实现多态的效果,第一必须是基类的指针或者引用,因为只有基类指针或者引用才即可以指向基类的对象又可以指向派生类对象。第二派生类必须对基类的虚函数重写/覆盖,只有重写/覆盖之后,派生类才能有不同的形态,达到多态的效果。

 2.1.2 虚函数

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

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

这里的virtual与虚继承的vircual式一个关键字,但是不同的作用,我们一定要区分清楚。

2.1.3 虚函数的重写/覆盖

        若派生类和基类有一个完全相同的基函数(要求三同,即函数返回值相同,函数名相同,函
        数参数的个数及类型和顺序相同),称派生类的虚函数重写了基类的虚函数。

        注意: 在重写基类虚函数时候,派生类虚函数在不加virtual的情况下,也构成重写,因为派
        生类把基类继承下来了,其依然保持虚函数的属性),但这种写法不规范,也不推荐,但这
        是比试中的一大坑点,需要注意一下。

2.1.4 多态场景的⼀个选择题

以下程序输出结果是什么()

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;
}

这是一道非常经典的面试题,许多大厂曾经都考过。

        首先,调用了test函数之后又调用了fun函数,这里我们先要明确的一点是这个类的成员函数因为是在A的类域里面,使用调用的是A的this指针,这符合了构成多态的第一条规则,即调用了基类的指针。
        第二我们可以发现基类的虚函数fun和派生类的有着相同的返回值,函数名,以及参数列表,因此也符合了构成多态的第二条规则,因此fun函数构成了多态。
        构成多态之后,由于调用的是 p->test() ,p是B类型的指针,因此调用的是派生类的fun函数。
        但这里还有一个坑点,由于这两个函数的虚函数参数的缺省值不同,可能很多人都会认为调用的是 val =0 的缺省值。但虚函数的重写/覆盖规则,原理是将基类的虚函数覆盖到派生类的虚函数,因此这里的缺省值用的其实是1,所以最后输出的是 B->1 。

2.1.5 虚函数重写的⼀些其他问题

协变

        派生类重写基类虚函数时,若满足“二同”(即函数名,参数列表相同,但是函数的返回值不
        同)且基类虚函数返回基类对象的指针或者引用,派⽣类虚函数返回派⽣类对象的指针
        或者引⽤时,称为协变。协变的实际意义并不⼤,所以我们 了解⼀下即可。

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();
}
析构函数的重写

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

有以下程序

class A
{
public:virtual ~A(){cout << "~A()" << endl;}
};
class B : public A {
public:~B(){cout << "~B()->delete:" << _p << endl;delete _p;}
protected:int* _p = new int[10];
};
// 只有派⽣类B的析构函数重写了A的析构函数,下⾯的delete对象调⽤析构函数,才能
//构成多态,才能保证p1和p2指向的对象正确的调⽤析构函数。
int main()
{A* p1 = new A;A* p2 = new B;delete p1;delete p2;return 0;
}

 若基类的析构函数不加virtual,那么delete p2的时候只会调用析构A的析构函数。这就会导致吧B中申请的内存没有及时还给系统,造成内存泄漏。

 如何解决这样的问题?

        首先我们要明白delete的工作原理,首先调用对应的析构函数 p->destructor(),再调用重载的operator delete[ ]清理空间,所以我们可以得出问题出现在第一步,由于p1,p2都是A* 类型的指针,所以我们在基类A的析构函数前面加上virtual使其与派生类虚函数构成重写,只有重写之后形成了多态,才能保证根据指向的对象不同产生不同的行为,调用对应的析构函数。

析构函数需不需要重写? 这个问题⾯试中经常考察,⼤家⼀定要结合类似上面的样例才能讲清楚,为什么基类中的析构 函数建议设计为虚函数。

 2.1.6 override和final关键字

        override函数可以帮我们检测出是否重写。
        因为动态多态在编译期间是无法检测出问题的,只有在运行期间我们根据输入没有得出我们
        需要的结果时候才会发现错误,因此有了这个关键字之后我们在编译期间就可以调试出错
        误。

        如果我们不想让派 ⽣类重写这个虚函数,那么可以⽤final去修饰。

2.1.7 重载/重写/隐藏的对⽐

3. 纯虚函数和抽象类

        在虚函数的后面加上 “ = 0 ” ,这个函数就被叫做纯虚函数。纯虚函数不需要定义实现(实现没啥意义因为要被 派⽣类重写,但是语法上可以实现),只要声明即可。有包含了纯虚函数的类被称为抽象类,抽象类不能实例化出对象,若派生类当中无纯虚函数,但是继承的基类当中有纯虚函数,那么这个派生类也是抽象类。

        纯虚函数在某种意义上强制了派生类重写虚函数,因为如果不重写的就实例化不出对象。

下面举一个简单的例子

        比如我们创建一个汽车类,基类是汽车,派生类是具体的品牌,我们不希望基类实例化出对象,因为对单独的车实例化的对象没意义,因此我们便在基类Car中写一个纯虚函数使其变为抽象类。

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()
{// 编译报错:error C2259: “Car”: ⽆法实例化抽象类 Car car;Car* pBenz = new Benz;pBenz->Drive();Car* pBMW = new BMW;pBMW->Drive();return 0;
}

4. 多态的原理

4.1 虚函数表指针

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;
}

对于以上的程序,可能会有人认为输出是8字节大小,但是,实际上输出大小是12字节/16字节。

b对象中除了成员变量 _h 和 _ch 还多了一个 指针 _vfptr,我们称其为虚函数表指针。一个含有虚函数的类至少都有一个虚函数表指针,因为这个类中所有的虚函数的地址都会被放在这个虚函数表指针指向的一个指针数组也就是虚函数表中,虚函数也简称虚表。

4.2 多态的原理

 4.2.1 多态是如何实现的

class Person {
public:virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:virtual void BuyTicket() { cout << "买票-打折" << endl; }
};
class Soldier : public Person {
public:virtual void BuyTicket() { cout << "买票-优先" << endl; }
};
void Func(Person* ptr)
{// 这⾥可以看到虽然都是Person指针Ptr在调⽤BuyTicket // 但是跟ptr没关系,⽽是由ptr指向的对象决定的。 ptr->BuyTicket();
}
int main()
{// 其次多态不仅仅发⽣在派⽣类对象之间,多个派⽣类继承基类,重写虚函数后 // 多态也会发⽣在多个派⽣类之间。 Person ps;Student st;Soldier sr;Func(&ps);Func(&st);Func(&sr);return 0;
}

        从底层来看,上述代码的Func函数中的ptr->BuyTicket(),是如何做到ptr指向Person对象就调用Person对象的BuyTicket,指向Student对象就调用Student对象的BuyTickrt函数的呢?

        在4.1中我们提到过,每一个包含了虚函数的类中都有一个虚函数表指针(也就是虚表)存放着这个类中所有的虚函数的地址。在满足了多态的条件之后,底层就不再是编译的时候通关调用对象来确定函数的地址了,而是通过运行时指向的对象来确定对应对象的虚表中对应的虚函数地址。

        这样就实现了指针指向基类就调用基类的虚函数,指向派生类就调用派生类的虚函数。

下图调用的是Person对象虚表中的虚函数。

下列对象调用的是Student对象中的虚函数。 

我们可以看到这两个函数虽然同名,但是是存放在了不同的地址。

4.2.2 动态绑定与静态绑定 

        对于不满足多态条件的函数的调用时在编译时绑定,也就是在编译时确定函数的地址,这叫做静态绑定。

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

下面时汇编层面的代码演示

// ptr是指针+BuyTicket是虚函数满⾜多态条件。 
// 这⾥就是动态绑定,编译在运⾏时到ptr指向对象的虚函数表中确定调⽤函数地址 
ptr->BuyTicket();
00EF2001 mov eax,dword ptr [ptr] 
00EF2004 mov edx,dword ptr [eax] 
00EF2006 mov esi,esp 
00EF2008 mov ecx,dword ptr [ptr] 
00EF200B mov eax,dword ptr [edx] 
00EF200D call eax// BuyTicket不是虚函数,不满⾜多态条件。 // 这⾥就是静态绑定,编译器直接确定调⽤函数地址 ptr->BuyTicket();00EA2C91 mov ecx,dword ptr [ptr] 00EA2C94 call Student::Student (0EA153Ch)

4.2.3 虚函数表

         所有的虚函数都会存在虚函数表当中。

        派生类有两部分构成,继承下来的基类和在自己的成员,⼀般情况下,继承下来的基类中有虚函数表 指针,⾃⼰就不会再⽣成虚函数表指针。但是要注意的这⾥继承下来的基类部分虚函数表指针和基 类对象的虚函数表指针不是同⼀个,就像基类对象的成员和派⽣类对象中的基类对象成员也独⽴ 的。

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

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

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

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

        虚函数表存在哪的?这个问题严格说并没有标准答案C++标准并没有规定,我们写下⾯的代码可以 对⽐验证⼀下。vs下是存在代码段(常量区)。

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

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

相关文章

js实现blob类型转化为excel文件

需求 后端通过接口将excel文件通过blob类型数据进行返回&#xff0c;前端接收数据并将其转化为excel文件进行下载 实现 接口方法 responseType&#xff1a;值为blob&#xff0c;标记返回数据类型为blob Content-Type&#xff1a;请求头设置&#xff0c;值为application/vnd…

融云「北极星」专业版:指标异常及时告警,趋势变化预先知悉

说起程序员的痛苦时刻&#xff0c;深夜接到告警电话、短信绝对榜上有名&#xff0c;甚至可能留下“铃声 PTSD”。 这也从另一个侧面提醒我们&#xff0c;所有在前台给用户丝滑体验的互联网产品&#xff0c;背后都有庞杂的系统和大量的工程师在支撑。而这其中&#xff0c;监控平…

安全篇(1)判断安全固件

判断安全固件的方法 一、通过串口开机打印 改方法适用Android与Tina 1.开机打印为SBOOT为安全 [289]HELLO! SBOOT is starting! 2.开机打印boot0为非安全 [88]BOOT0 commit : 1cbb5ea8b3 二、通过读数据 1.getprop | grep verifiedbootstate 这条命令的输出表示设备的…

火山引擎VeDI数据服务平台:在电商场景中,如何解决API编排问题?

01 平台介绍 数据服务平台可以在保证服务高可靠性和高安全性的同时&#xff0c;为各业务线搭建数据服务统一出口&#xff0c;促进数据共享&#xff0c;为数据和应用之间建立了一座“沟通桥梁”。 同时&#xff0c;解决数据理解困难、异构、重复建设、审计运维困难等问题&#x…

在Ubuntu 上实现 JAR 包的自启动

在 Ubuntu 上实现 JAR 包的自启动&#xff0c;可以通过以下几种方法&#xff1a; 方法一&#xff1a;使用 systemd 创建一个服务文件&#xff1a; 在 /etc/systemd/system/ 目录下创建一个新的服务文件&#xff0c;例如 myapp.service&#xff1a; sudo nano /etc/systemd/sys…

Object 内部类 异常

Objbect类 java提供了Object,它是所有类的父类,每个类都直接或间接的继承了Object类,因此Object类通常被称为超类 当定义一个类时,如果没有使用extends关键字直接去指定父类继承,只要没有被继承的类,都是会默认的去继承Object类,超类中定义了一些方法 方法名称方法说明boole…

SQLite -- 一个遵守君子协定的数据库

用惯了Oracle、PostgreSQL等数据库&#xff0c;今天接触到SQLite&#xff0c;简单尝试了下使用&#xff0c;顿感震惊&#xff01;&#xff01;&#xff01; 与传统的关系型数据库&#xff08;如 MySQL、PostgreSQL 等&#xff09;相比&#xff0c;它的约束是真的宽松。具体来说…

Linux 高级IO

学习任务&#xff1a; 高级 I/O&#xff1a;select、poll、epoll、mmap、munmap 要求&#xff1a; 学习高级 I/O 的用法&#xff0c;并实操 1、高级 I/O&#xff1a; 前置知识&#xff1a; 阻塞、I/O 多路复用 PS: 非阻塞 I/O ------ 非阻塞 I/O 阻塞其实就是进入了休眠状态&am…

JAVA WEB — HTML CSS 入门学习

本文为JAVAWEB 关于HTML 的基础学习 一 概述 HTML 超文本标记语言 超文本 超越文本的限制 比普通文本更强大 除了文字信息 还可以存储图片 音频 视频等标记语言 由标签构成的语言HTML标签都是预定义的 HTML直接在浏览器中运行 在浏览器解析 CSS 是一种用来表现HTML或XML等文…

ASRPRO 日历2

为避免与天问的ID冲突 ID前加10000 为使识别更顺畅 将 日期-月份 12月21日 合并 ;时间 10点25分 合并 通过串口获取日期 为使用常用词 计倒时 下周 明天,需通过串口获取当前日期 + 命令词 增加 我的 A的 B的 关系词 与任务 生日 买菜 增加 可自定义 任务 执行程序 双进…

Linux——Linux基础指令

Linux基本指令 文章目录 Linux基本指令1. 基础五指令(1) whoami(2) who(3) pwd(4) ls(5) clear 2. 文件常见命令(1) touch(2) mkdir(3) cp(4) mv(5) rm(6) cd 3. 常见IO命令(1) cat(2) tac(3) head(4) tail(5) more(6) less 4. 拓展命令(1) man手册(2) which(3) file(4) date(5…

雷池社区版 7.1.0 LTS 发布了

LTS&#xff08;Long Term Support&#xff0c;长期支持版本&#xff09;是软件开发中的一个概念&#xff0c;表示该版本将获得较长时间的支持和更新&#xff0c;通常包含稳定性、性能改进和安全修复&#xff0c;但不包含频繁的新特性更新。 作为最受欢迎的社区waf&#xff0c…

出海企业如何借助云计算平台实现多区域部署?

云计算de小白 如需进一步了解&#xff0c;请单击链接了解有关 Akamai 云计算的更多信息 在本文中我们将告诉大家如何在Linode云计算平台上借助VLAN快速实现多地域部署。 首先我们需要明确一些基本概念和思想&#xff1a; 部署多区域 VLAN 为了在多区域部署中在不同的 VLAN …

RDD转换算子:【map】

功能&#xff1a; 对RDD中每个元素调用一次参数中的函数&#xff0c;并将每次调用的返回值放入一个新的RDD中&#xff08;一对一&#xff09; 语法&#xff1a; def map(self , f: T -> U ) -> RDD[U]f&#xff1a;代表参数是一个函数 T&#xff1a;代表RDD中的每个元…

如何更好的crud

一、DDD是什么&#xff1f; DDD全名叫做Domins drives Design&#xff1b;领域驱动设计。再说的通俗一点就是&#xff1a;通过领域建模的方式来实现软件设计。 问题来了&#xff1a;什么是软件设计&#xff1f;为什么要进行软件设计&#xff1f; 软件开发最主要的目的就是&…

AI赋能酒店设计|莱佛士学生成功入围WATG设计大赛

近日&#xff0c;由Wimberly Allison Tong & Goo&#xff08;WATG&#xff09;主办的“用人工智能重新构想酒店行业的未来”设计比赛正式拉开帷幕。这场设计比赛&#xff0c;不仅是为了庆祝WATG即将步入80周年&#xff0c;更是为了激发年轻设计师们的创造力和探索实践精神&…

Netty原来就是这样啊(二)

前言: Netty其实最大的特点就是在于对于对NIO进行了进一步的封装,除此以外Netty的特点就是在于其的高性能 高可用性,下面就会一一进行说明。 高性能: 我在Netty原来就是这样啊(一)-CSDN博客 解释了其中的零拷贝的技术除此以外还有Reactor线程模型,这个Reactor线程模型的思想…

对于相对速度的重新理解

狭义相对论速度合成公式如下&#xff0c; 现在让我们尝试用另一种方式把它推导出来。 我们先看速度的定义&#xff0c; 常规的速度合成方式如下&#xff0c; 如果我们用速度的倒数来理解速度&#xff0c; 原来的两个相对速度合成&#xff0c; 是因为假定了时间单位是一样的&am…

idea 导入Spring源码遇到的坑并解决

1.下载相关文件 通过百度网盘分享的文件&#xff1a;Spring 链接&#xff1a;https://pan.baidu.com/s/1r9rkGOCaY9SFn9ecng5cIg?pwd8888 提取码&#xff1a;8888 2.配置gradle环境 gradle下载地址 需要翻墙下 https://services.gradle.org/distributions/ 我选择的是 grad…

红队-linux基础(1)

声明 通过学习 泷羽sec的个人空间-泷羽sec个人主页-哔哩哔哩视频,做出的文章如涉及侵权马上删除文章 笔记的只是方便各位师傅学习知识,以下网站只涉及学习内容,其他的都与本人无关,切莫逾越法律红线,否则后果自负 一.openssl 1、openssl passwd -1 123 openssl是一个开源的…