从零开始的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.rhkb.cn/news/465361.html

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

相关文章

linux node vue3 部署手册

第一步&#xff1a;在linux 系统中安装node 1、在网址&#xff1a;https://nodejs.org/dist/ 下载对应版本的安装包。 2、解压缩下载的压缩包到任意位置&#xff0c;推荐home下。 样例路径为&#xff1a;/home/syl/node-v20.17.0-linux-x64.tar.xz 样例&#xff1a; tar -xv…

探索C/C++的奥秘之string类

string叫串&#xff0c;是一个管理字符数组的类&#xff0c;其实就是一个字符数组的顺序表&#xff0c;通过成员函数对字符串进行增、删、查、改。 C标准库里面的东西都在std这个命名空间中。 int main() { string s1; std:: string s2; std::string name("x…

【刷题】优选算法

优选算法 双指针 202. 快乐数 链接&#xff1a;. - 力扣&#xff08;LeetCode&#xff09; 【思路】 第一个实例是快乐数&#xff0c;因为会变为1且不断是1的循环 第二个实例不可能为1&#xff0c;因为会陷入一个没有1的循环 根据两个实例和鸽巢原理可以发现不断的平方和最…

openEuler的aarch64操作系统上安装k3s

1、需要安装docker容器引擎&#xff08;省略&#xff09; 2、安装ks3命令 curl -sfL https://rancher-mirror.rancher.cn/k3s/k3s-install.sh | INSTALL_K3S_MIRRORcn INSTALL_K3S_SKIP_SELINUX_RPMtrue INSTALL_K3S_SELINUX_WARNtrue sh -s -- --docker 其中&#xff1a…

Synchronized锁、锁的四种状态、锁的升级(偏向锁,轻量级锁,重量级锁)

目录 1. Synchronized锁 1.1 介绍 1.2 三种应用方式★ 1.2.1 synchronized同步方法 1.2.2 synchronized 同步静态方法 1.2.3 synchronized 同步代码块 1.3 Synchronized锁底层原理 1.3.1 简答 1.3.2 详述 1. Monitor对象 2. Monitor与对象锁关联时 具体的流程&#…

【网络】数据链路层

目录 以太网 以太网的帧格式 MSS 交换机 MTU对UDP的影响 ARP协议 数据链路层是软件层的最底层协议&#xff0c;它的下面就是物理层&#xff0c;那么下面我们就来介绍一下它负责在网络通信中完成什么工作 我们前面说的IP协议是解决如何进行跨网络转发的&#xff0c;也就是…

零基础‘自外网到内网’渗透过程详细记录(cc123靶场)——下

细节较多&#xff0c;篇幅较大&#xff0c;分为上/下两部分发布在两篇文章内 另一部分详见下面文章 零基础‘自外网到内网’渗透过程详细记录(cc123靶场)——上https://blog.csdn.net/weixin_62808713/article/details/143572185 八、第二层数据库服务器权限获取 猜到新闻资…

13-鸿蒙开发中的综合实战:华为登录界面

大家好&#xff0c;欢迎来到鸿蒙开发系列教程&#xff01;今天&#xff0c;我们将通过一个综合实战项目来实现一个华为登录界面。这个项目将涵盖输入框组件、按钮组件、文本组件和布局容器的使用&#xff0c;帮助你更好地理解和应用这些组件。无论你是初学者还是有一定经验的开…

告别复杂协作:Adobe XD的简化替代方案

Adobe XD是一款集成UI/UX设计和原型创建功能的设计平台。它允许用户进行网页、移动应用的设计&#xff0c;以及原型的绘制&#xff0c;并且能够将静态设计转化为动态的交互原型。尽管Adobe XD提供了这些功能&#xff0c;但它依赖于第三方插件&#xff0c;且插件库有限&#xff…

ctfshow web文件上传 web166-170

1.web166 通过源码上传发现只能传zip&#xff0c;尝试一下图片上传也不行 把随便一张图片打包成zip文件&#xff0c;上传后发现有一个下载的地方,猜测是文件上传&#xff0c;尝试zip伪协议发现失败&#xff0c;打包php文件也失败了&#xff0c;不知为什么&#xff0c;&#x…

二开CS—上线流量特征shellcode生成修改模板修改反编译打包

前言 免杀几乎讲的差不多了&#xff0c;今天讲个CS的二次开发。我们原生态的CS特征肯定都是被提取完的了&#xff0c;包括它的流量特征&#xff0c;而我们要做的就是把它的流量特征给打乱&#xff0c;还可以修改生成的后门&#xff0c;使其生成即免杀。 实验环境 CS4.4&…

7.《双指针篇》---⑦三数之和(中等偏难)

题目传送门 方法一&#xff1a;双指针 1.新建一个顺序表用来返回结果。并排序数组。 2.for循环 i 从第一个数组元素遍历到倒数第三个数。 3.如果遍历过程中有值大于0的则break&#xff1b; 4.定义左右指针,以及target。int left i 1, right n - 1; int target -nums[i];…

Muse-Ant-Desgin-Vue 改造成 Vite+Vue3

后台地址&#xff1a;https://www.creative-tim.com/product/muse-vue-ant-design-dashboard?refantdv-official 一、配置 ViteAntDesginVue 配置ViteAntDesginVue ViteAntDesginVue配置&#xff1a;https://blog.csdn.net/qq_17523181/article/details/143241626 安装vue-ro…

实习作假:阿里健康实习做了RABC中台,还优化了短信发送流程

最近有二本同学说&#xff1a;“大拿老师&#xff0c;能帮忙看下简历吗&#xff1f;” 如果是从面试官的角度来看&#xff0c;这个同学的实习简历是很虚假的。 但是我们一直强调的是&#xff1a;校招的实习简历是不能出现明显的虚假。 首先&#xff0c;你去公司做事情&#…

疯狂Java讲义-Java基础类库

Java基础类库 本章思维导图 5-0Java基础类库.png 用户互动 使用Scanner获取键盘输入 Scanner主要提供了两个方法来扫描输入 hasNextXxx(); 是否还有下一个输入项&#xff0c;其中Xxx可以是int、long等代表基本数据类型的字符串。 nextXxx(); 获取下一个输入项。Xxx的含义与前一…

[前端] 为网站侧边栏添加搜索引擎模块

前言 最近想给我的个人网站侧边栏添加一个搜索引擎模块&#xff0c;可以引导用户帮助本站SEO优化&#xff08;让用户可以通过点击搜索按钮完成一次对本人网站的搜索&#xff0c;从而实现对网站的搜索引擎优化&#xff09;。 最开始&#xff0c;我只是想实现一个简单的百度搜索…

汇聚全球前沿科技产品,北京智能科技产业展览会·世亚智博会

在北京这座古老而又充满现代气息的城市中&#xff0c;一场科技与创新的盛宴正悄然上演——北京智能科技产业展览会&#xff08;简称&#xff1a;世亚智博会&#xff09;&#xff0c;作为全球前沿科技的汇聚地&#xff0c;不仅展示了人工智能、5G通信、虚拟现实等尖端技术的最新…

JAVA基础:数组 (习题笔记)

一&#xff0c;编码题 1&#xff0c;数组查找操作&#xff1a;定义一个长度为10 的一维字符串数组&#xff0c;在每一个元素存放一个单词&#xff1b;然后运行时从命令行输入一个单词&#xff0c;程序判断数组是否包含有这个单词&#xff0c;包含这个单词就打印出“Yes”&…

猎板PCB2到10层数的科技进阶与应用解析

1. 单层板&#xff08;Single-sided PCB&#xff09; 定义&#xff1a;单层板是最基本的PCB类型&#xff0c;导线只出现在其中一面&#xff0c;因此被称为单面板。限制&#xff1a;由于只有一面可以布线&#xff0c;设计线路上有许多限制&#xff0c;不适合复杂电路。应用&…

2025年山东省考报名流程图解

2025年山东公务员考试备考开始 为大家整理了从笔试到录用的全部流程&#xff0c;希望可以帮助到你们&#xff01;参考2024年山东省考公告整理&#xff0c;请以最新公告为准&#xff01; 一、阅读公告和职位表 二、职位查询 三、网上报名 四、确认缴费 五、网上打印准考证 六、参…