【C++】多态的原理

news2025/3/4 12:20:06

目录

一、虚函数表 

1、虚函数表的定义

 2、虚函数表特性

3、虚表的打印

二、多态的原理

三、多态的相关问题

1、指针偏移问题

2、输出的程序是什么?

3、输出的程序是什么?


【前言】

上一篇我们学习了多态的基础知识,这一篇我将带着大家深入多态学习,了解多态的原理。【多态的基本介绍】

一、虚函数表 

1、虚函数表的定义

// 这里常考一道笔试题:sizeof(Base)是多少?
class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}

private:
	int _b = 1;
	char _ch;
};

int main()
{
	cout << sizeof(Base) << endl;
    Base bb;
	return 0;
}

很多人会因为内存对齐认为答案是8,其实不然,答案是12.

这是因为类里面,除了_bb成员,还多一个__vfptr 放在对象的前面(注意有些平台可能会放到对象的最后面,这个跟平台有关),对象中的这个指针我们叫做虚函数表,存的是虚函数的地址。指针(v 代表 virtual,f 代表 function)。一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表,本质是一个虚函数指针数组,一般情况这个数组最后面放了一个nullptr

 2、虚函数表特性

派生类的虚表是如何形成的呢?

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

a.先将基类中的虚表内容拷贝一份到派生类虚表中

b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数

c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。

d.要理解将子类赋值给父类对象时,切片过程中,子类的虚表并没有拷贝切过去。这个过程是不会拷贝子类的虚表的。因为如果拷贝子类的虚表赋值给父类了,那么当指向的对象是这个父类时,到这个父类的虚表里找,找的那就是子类的虚函数了,而不是父类的虚函数

多态是如何利用虚函数表实现指向父类调用父类函数,指向子类调用子类函数的呢?

当指向父类对象时,就会去父类对象的虚表里找虚函数,当指向子类对象时,就会去子类对象的虚表里找虚函数。
中间发生了切割,本质上都是指向了父类数据,看到的还是父类对象。因为派生类继承不仅继承父类的所有数据,也将父类的虚表继承下来了派生类会将重写的虚函数地址覆盖原来的基类的虚函数。这样就可以实现指向父类调用父类函数,指向子类调用子类函数。

同种类型的函数会被放在同一个虚函数表,同类型的对象会指向同一个虚表

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

我们知道要达到多态,有两个条件,一个是虚函数覆盖,一个是对象的指针或引用调用虚函数。反思一下为什么一定要满足这个条件呢?为什么函数直接调用不行呢?

当使用指针或者引用时,父类的对象会指向父类的虚表,子类会继承父类的接口并进行重写指向子类的虚表。这样就可以实现指向父类调用父类函数,指向子类调用子类函数。

对于对象来说,指向父类调用父类函数,指向子类时会面临一个问题,父类的虚表会不会被子类拷贝,如果不拷贝,父类成员的虚表里面永远只有父类的虚函数,这显然是不行的。如果拷贝,虚表指向不明确,是原本父类的虚表还是子类拷贝过来的虚表。所以对象的切片只拷贝成员,不拷贝虚表。

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

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

虚表是什么阶段生成的?对象中虚表指针什么时候初始化?

虚表是在编译过程中生成的,因为编译过程中会生成地址。虚表指针是在构造函数中通过初始化列表中初始化。 

理解虚函数为什么要重写?

只有虚函数重写,派生类的虚表里才可以存真正派生类虚函数,因为这个虚表是从父类继承下来的,里面都是父类的虚函数地址。而只有派生类虚函数重写后,才可以将重写的虚函数地址覆盖上去。这样就可以做到指向父类调用父类虚表中对应的虚函数,指向子类,调用子类虚表中对应的虚函数。

3、虚表的打印

 有时候监视器窗口虚表不一定全部显示出来,所以我们可以写一个打印虚表的代码,便于我们自己观察。

虚表是一个函数指针数组,但是函数指针的类型比较复杂,所以我们重定义一下函数指针,增强代码可读性,需要注意:typedef void(*)() VF_PTR 函数指针定义名字需要放在中间

 不同对象虚表里面对象不一样多,vs编译器在虚表最后都会放置一个空,所以我们可以利用这个原理实现for循环。

二、多态的原理

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 mike;
	Func(mike);
	Student johnson;
	Func(johnson);
	return 0;
}

当p是指向mike对象时,p->BuyTicket在mike的虚表中找到虚函数是Person::BuyTicket。当p是指向johnson对象时,p->BuyTicket在johson的虚表中找到虚函数是Student::BuyTicket。 这样就实现出了不同对象去完成同一行为时,展现出不同的形态。

我们需要知道满足多态以后的函数调用,不是在编译时确定的,是运行起来以后到对象的中取找的。不满足多态的函数调用时编译时确认好的。

三、多态的相关问题

1、指针偏移问题

class Base1 {  public:  int _b1; };
 class Base2 {  public:  int _b2; };
 class Derive : public Base1, public Base2 { public: int _d; };
 int main(){
 Derive d;
 Base1* p1 = &d;
 Base2* p2 = &d;
 Derive* p3 = &d;
 return 0;
 }

 A:p1 == p2 == p3   B:p1 < p2 < p3    C:p1 == p3 != p2   D:p1 != p2 != p3

【答案】选C 

2、输出的程序是什么?

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

 A: A->0    B: B->1   C: A->1   D: B->0   E: 编译出错    F: 以上都不正确

 【答案】选B

首先判断是否形成多态:

1、虚函数的重写--三同(函数名、参数、返回值)2、父类指针或者引用调用。

是多态。p调用 test函数,test 调用 func函数,func 函数参数的类型是A* this,this 调用class B的 func,派生类继承父类的成员函数,也会继承成员函数的缺省参数,所以val=1.

3、输出的程序是什么?

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

 A: A->0    B: B->1   C: A->1   D: B->0   E: 编译出错    F: 以上都不正确

【答案】选D

首先判断是否形成多态:

1、虚函数的重写--三同(函数名、参数、返回值)2、父类指针或者引用调用。

不是多态,test 直接在class B调用func函数。

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

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

相关文章

InputStreamReader类详解

咦咦咦&#xff0c;各位小可爱&#xff0c;我是你们的好伙伴——bug菌&#xff0c;今天又来给大家普及Java SE相关知识点了&#xff0c;别躲起来啊&#xff0c;听我讲干货还不快点赞&#xff0c;赞多了我就有动力讲得更嗨啦&#xff01;所以呀&#xff0c;养成先点赞后阅读的好…

刷题之动态规划

前言 大家好&#xff0c;我是jiantaoyab&#xff0c;开始刷动态规划的题目了&#xff0c;要特别注意初始化的时候给什么值。 动态规划5个步骤 状态表示 &#xff1a;dp数组中每一个下标对应值的含义是什么->dp[i]表示什么状态转移方程&#xff1a; dp[i] 等于什么1 和 2 是…

C++项目——集群聊天服务器项目(六)MySQL模块

Hello&#xff0c;大家好啊&#xff0c;最近比较忙&#xff0c;没来得及更新项目&#xff0c;实在抱歉~今天就恢复更新拉~ 在验证完网络模块与业务模块代码可以正常使用后&#xff0c;需完成的操作是与底层数据库进行交互&#xff0c;为实现各类用户查询、增删业务奠定良好的基…

C语言goto语句介绍

在C语言中&#xff0c;goto语句是一种流程控制语句&#xff0c;用于无条件地转移到程序中的特定标签位置。尽管goto语句在编程中具有一定的争议&#xff0c;但在某些情况下&#xff0c;它可以提供一种简单有效的解决方案。本文将深入介绍C语言中的goto语句&#xff0c;包括其基…

量化交易入门(二十九)布林带指标实现和回测

首先我们来看一张图&#xff0c;这张图就是拿的苹果股票2020年1月1日到2023年12月30日的历史数据进行回测后生成的。图中绿色箭头是买入点&#xff0c;红色箭头是卖出点。我们看到大部分的时候是在股价较低的时候买入&#xff0c;在股价较高的时候卖出&#xff0c;好像挺不错的…

K8s Pod亲和性、污点、容忍度、生命周期与健康探测详解(中)

&#x1f407;明明跟你说过&#xff1a;个人主页 &#x1f3c5;个人专栏&#xff1a;《Kubernetes航线图&#xff1a;从船长到K8s掌舵者》 &#x1f3c5; &#x1f516;行路有良友&#xff0c;便是天堂&#x1f516; 在上一章节中&#xff0c;我们详细探讨了Pod的亲和性&…

计算机网络(第八版)-第1章课后习题参考答案

计算机网络(第八版)-第1章课后习题参考答案 本文是对自己之前文章的格式化&#xff1a;https://blog.csdn.net/qq_46396470/article/details/132788972?spm1001.2014.3001.5502 T1-01 计算机网络向用户可以提供哪些服务&#xff1f; 连通性和共享 &#xff0c;例如音频&…

Lecture 1 - Introduction

Lecture 1 - Introduction MIT 6.824 Distributed Systems 1、概念预览 分布式系统需要考虑的因素&#xff1a; Parallelism &#xff1a;并行性Fault tolerence &#xff1a;容错性Physicial &#xff1a;不同系统之间物理距离引起的通信问题**Security ** &#xff1a;不…

WPF 路由事件 数据驱动 、Window 事件驱动

消息层层传递&#xff0c;遇到安装有事件侦听器的对象&#xff0c;通过事件处理器响应事件&#xff0c;并决定事件是否继续传递&#xff1b; 后置代码中使用AddHandler方法设置事件监听器&#xff0c;该方法的 第一个参数是指定监听的路由事件类型对象&#xff0c; 第二个参数…

3.29 day1 freeRTOS

1.总结keil5下载代码和编译代码需要注意的事项 注意要将魔术棒的的debug选项中的setting中的flashdownload中的reset and run 勾选上&#xff0c;同时将pack中的enable取消勾选 2.总结STM32Cubemx的使用方法和需要注意的事项 可以通过功能列表对引脚进行设置&#xff0c;并且可…

153 Linux C++ 通讯架构实战8 ,日志打印实战,设置时区,main函数中顺序调整

日志打印实战 //日志的重要性&#xff1a;供日后运行维护人员去查看、定位和解决问题&#xff1b; //新文件&#xff1a;ngx_printf.cxx以及ngx_log.cxx。 //ngx_printf.cxx&#xff1a;放和打印格式相关的函数&#xff1b; //ngx_log.cxx&#xff1a;放和日志相关…

[Python人工智能] 四十五.命名实体识别 (6)利用keras构建CNN-BiLSTM-ATT-CRF实体识别模型(注意力问题探讨)

从本专栏开始,作者正式研究Python深度学习、神经网络及人工智能相关知识。前文讲解融合Bert的实体识别研究,使用bert4keras和kears包来构建Bert+BiLSTM-CRF模型。这篇文章将详细结合如何利用keras和tensorflow构建基于注意力机制的CNN-BiLSTM-ATT-CRF模型,并实现中文实体识别…

bugku-web-本地管理员

在页面源码的最右段发现一个base加密数据 dGVzdDEyMw 解密后 test123 将test作为账号123为密码尝试 说这个ip被禁止访问 那么问题应该出现在ip上 这里将请求报文抓取下来 POST / HTTP/1.1 Host: 114.67.175.224:18838 Content-Length: 18 Cache-Control: max-age0 Upgrade-Ins…

CTF题型 php://filter特殊编码绕过小汇总

CTF题型 php://filter特殊编码绕过小汇总 文章目录 CTF题型 php://filter特殊编码绕过小汇总特殊编码base64编码string过滤器iconv字符集 例题1.[Newstarctf 2023 week2 include]2.[Ctfshow web 117] php://filter 是一个伪协议&#xff0c;它允许你读取经过过滤器处理的数据流…

MHA 高可用配置 及故障切换

目录 一 什么是 MHA 1, MHA 概念 2&#xff0c;MHA 架构 2,1 传统mysql 架构存在问题 2.2 MHA高可用 3&#xff0c;MHA 的组成 4&#xff0c;数据同步方式 4.1 同步复制 4.2 异步复制 ​编辑 4.3 半同步复制 5&#xff0c;MHA 的特点 6&#xff0c; MHA工作原理…

OpenAI发布Voice Engine模型!用AI合成你的声音!

大家好&#xff0c;我是木易&#xff0c;一个持续关注AI领域的互联网技术产品经理&#xff0c;国内Top2本科&#xff0c;美国Top10 CS研究生&#xff0c;MBA。我坚信AI是普通人变强的“外挂”&#xff0c;所以创建了“AI信息Gap”这个公众号&#xff0c;专注于分享AI全维度知识…

解决WSL更新速度慢的方案

在Windows上安装Docker Desktop时&#xff0c;如果选择使用WSL&#xff0c;则可能会出现在运行程序前要求升级WSL的步骤。程序会提示使用下面指令来升级 wsl.exe --update但是升级速度特别慢&#xff0c;于是在网络不稳定的情况下经常会出现下载失败的情况。 百度里一直没搜到…

shell的工作原理

本文旨在讲解shell的工作原理&#xff0c;希望读完本文&#xff0c;能使读者对shell的工作原理有一定的认识&#xff0c;废话不多说&#xff0c;开唠&#xff01; 在讲解shell的工作原理之前&#xff0c;我要首先给大家讲一下什么是操作系统&#xff0c;以Linux操作系统为例&am…

【Springboot整合系列】SpringBoot整合WebService

目录 Web服务介绍Web服务的两种类型Web服务架构Web服务的主要特点Web服务使用场景Web服务标准和技术 WebService介绍WebService的作用适用场景不适用场景 WebService的原理三个角色相关概念 WebService开发框架代码实现服务端1.引入依赖2.实体类3.业务层接口接口实现类 4.配置类…

vue中使用图片url直接下载图片

vue中使用图片url直接下载图片 // 下载图片downloadByBlob(url, name) {let image new Image()image.setAttribute(crossOrigin, anonymous)image.src urlimage.onload () > {let canvas document.createElement(canvas)canvas.width image.widthcanvas.height image…