【C++技能树】多态解析

2023-11-16

在这里插入图片描述
Halo,这里是Ppeua。平时主要更新C++,数据结构算法,Linux与ROS…感兴趣就关注我bua!

在这里插入图片描述

0.多态的概念

试想下这个场景,不同身份的人去买票,相同的函数会执行不同的行为.这就需要多态去完成

多态:顾名思义一个类中函数的多种状态.

先来看看下面这个例子:

class Person{
public:
    virtual void BuyTicket()
    {
        cout<<"买票全价"<<endl;
    }
    virtual ~Person()
    {
        cout<<"~person()"<<endl;
    }
};
class Student:virtual public Person
{
public:
    virtual void BuyTicket()
    {
        cout<<"买票半价"<<endl;
    }
    virtual ~Student()
    {
        cout<<"~student()"<<endl;
    }
};
void buyticket(Person *p1)
{
    p1->BuyTicket();   
}

当在buyticket中传入student类型的地址与传入Person类型的地址会执行不同的行为.

传入Person:会输出买票全价

传入Student:会输出买票半价

这就是多态的具体行为.会根据传入对象的不同执行不同的行为.

0.1 多态的定义

在基类需要重写的函数前加上virtual.

在派生类中想要达到重写的函数前也加上virtual.(可加可不加),之后保持与基类中函数相同的返回值,函数名,参数列表即可完成重写.

在调用时需要通过基类的指针或者引用来调用(将想要调用的类赋值到父类的指针或者引用,调用相同的函数.即可完成多态)

有一个例外:返回值可不一定需要相同,可以为父类或子类对象的指针或引用(要同为指针,或者同为引用),称为协变

所以多态就是:不同对象传递,调用不同的函数.多态调用看指向的对象.具体是什么内容 ,而不是看当前类型.

48410c494a8f224dac0bd406a27a6dd

1. 重写

析构函数无论加不加virtual都完成重写

class Person{
public:
    virtual Person& BuyTicket()
    {
        cout<<"买票全价"<<endl;
    }
     ~Person()
    {
        cout<<"~person()"<<endl;
    }
};
class Student:virtual public Person
{
public:
    virtual Student& BuyTicket() 
    {
        cout<<"买票半价"<<endl;
    }
     ~Student()
    {
        cout<<"~student()"<<endl;
    }
};

这是因为编译器对析构函数进行了处理,在编译阶段都重命名为了Destructor,所以他们为同名函数.

为什么要进行这么处理呢?

当用父类指针去调用子类对象时,使用delete时,若无多态则只会把父类的成员属性删除.并不会删除子类的.

Person* p=new Person;
delete p;
p=new Student;
//析构错误 不多态则没有调到派生类的析构
delete p;  //p->destructor+operator delete p

2.Final与Override

不想让一个函数被重写时可以在其后加上final,此时会从语法来检查该函数是否被重写.

检查一个函数是否满足重写的条件可以在其后加上override,用来检查是否满足重写的条件

class Person{
public:
    // virtual void BuyTicket() final
    // {
    //     cout<<"买票全价"<<endl;
    // }
    virtual Person& BuyTicket()
    {
        cout<<"买票全价"<<endl;
    }
     ~Person()
    {
        cout<<"~person()"<<endl;
    }
};
class Student:virtual public Person
{
public:

    virtual Student& BuyTicket() override
    {
        cout<<"买票半价"<<endl;
    }
     ~Student()
    {
        cout<<"~student()"<<endl;
    }
};

3.抽象类

多态也叫接口继承,也就是只继承基类的函数接口,内容自己重新写.普通函数的继承是一种实现继承

那么我们也可以设计一个只提供接口的类.那么就是抽象类

在一个虚函数中最后加上=0 则成为 纯虚函数,包含纯虚函数的类则称为抽象类.

class Car
{
public:
		virtual void Drive() = 0;
};

抽象类不能用来实例化对象,只能用来当作基类提供接口

4.多态中的内存分布.

根据上面的介绍,我们对多态的使用已经有了初步的了解.

即在重定义的基础上加上一个virtual 以及满足三同(同名 同参数 同返回值)

那么在内存中多态是如何存储的?

class Person{
public:
	virtual void fun()
	{
		cout << "hello person";
	}
};
class Student :public Person{
	virtual void fun()override {
		cout << "hello student";
	}
	virtual void fun3() {
		cout << "hello student3";
	}
};
int main()
{
	Person* p1;
	Student s1;
	p1 = &s1;
	p1->fun();
}

这是一个多态调用的例子.我们通过vs2022来看看其在内存中是如何存储的.

image-20230903123709063

可以看到,其有一个vfptr(virtual fun ptr)虚表指针.其和我们之前继承中的虚基表有点类似.当中存储了一个完成重写的函数.fun

我们在内存中输入这个地址,可以发现其存储了两段地址.

image-20230903124449931

  1. 第一个为完成重写的fun的地址

  2. 第二个为自己的虚函数fun3的地址,但这在上图中的结构模型中并没有被看到.所以结构模型有时并不是完全可信的

    所以,自己的虚函数会直接放在第一个虚表的最后.

  3. 第三个表示虚表的结束(在vs2022中是这样表示的)

    综上可以看出,在实例化的时候,会将基类的虚表复制一份到派生类当中,若有重写的函数,则用重写完的函数地址去覆盖虚表中原函数的地址.所以在原理层中:也叫做 覆盖.

4.1虚表存在哪里?

虚表是存储在 栈区 堆区 静态区 还是常量区呢?

我们可以通过以下这个函数来验证

int main()
{
	Person p1;
	Student s1;
	int a = 0;
	printf("栈:%p", &a);
	cout << endl;
	int* b = new int[10];
	printf("堆:%p", b);
	cout << endl;

	const char* c = "hello world";
	printf("常量区%p", c);
	cout << endl;

	static int d = 10;
	printf("静态区%p", &d);
	cout << endl;

	printf("虚表1:%p", *((int*)&p1));
	cout << endl;
	printf("虚表2:%p", *((int*)&s1));
	cout << endl;
}

为什么这样区能取到虚表地址呢?通过取对应对象的地址,之后在进行强制转换为int,此时访问宽度为前四个字节(因为一个指针大小为四个字节).之后在对这个指针进行解引用就为虚表的地址*

我们运行这段代码,就可以发现,虚表存储在常量区

image-20230903125353911

5.多态调用原理

上文我们已经知道了,如何去调用多态.以及虚表存储的模型.下面是一个多态调用例子:

class Person{
public:
	virtual void fun()
	{
		cout << "hello person";
	}
};
class Student :public Person{
	virtual void fun()override {
		cout << "hello student";
	}
};
int main()
{
	Person* p1;
	Student s1;
	p1 = &s1;
	p1->fun();
}

根据前面所学可以看出,当我调用p1->fun()时,由于p1里面存的是s1的地址,所以这里就会取到s1的虚表中f1的地址,完成多态调用

所以多态是在运行的时候动态确定需要执行的函数

5.1 动态绑定与静态绑定

  1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态.比如:函数重载

  2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态

6.继承中的虚函数表

下面将从两个部分:多继承与单继承中的虚函数表来划分

6.1单继承中的虚函数表

在上文中可以知道,自己定义的虚函数,还没被重写之前是不会被放进虚表的.所以我们要研究这个存储就必须手动访问函数地址.

我们先重命名下函数的指针,方便后期调用:

typedef void(*FUNC_PTR)();

注意:这里的函数返回值为void,参数为空.将其重命名为FUNC_PTR

所以我们通过这样的方式去强行访问类中虚表存储的函数

class Person{
public:
	virtual void fun1()
	{
		cout << "Person::fun1";
	}
	virtual void fun2()
	{
		cout << "Person::fun2";
	}
	virtual void fun3()
	{
		cout << "Person::fun3" ;
	}
};
class Student :public Person{
	virtual void fun1()override {
		cout << "Student::fun1()";
	}
	

	virtual void fun3()
	{
		cout << "Student::fun3()";
	}
	virtual void fun4()
	{
		cout << "Student::fun4()";
	}
};
typedef void(*FUNC_PTR)();
void printvft(FUNC_PTR* table)
{
	for (int i = 0; table[i] != nullptr; i++)
	{
		printf("%d->%p", i, table[i]);
		FUNC_PTR f=table[i];
		f();
		cout << endl;
	} 
}
int main()
{
	Student s1;
	Person p1;
	cout << "person:" << endl;
	int vft = *((int*)&p1);
	printvft((FUNC_PTR*)vft);
	cout << "student:" << endl;
	vft = *((int*)&s1);
	printvft((FUNC_PTR*)vft);
}

其中vft的赋值原理是,我们知道虚表地址是存在对象中的前四个位,所以我们用int*去取.之后解引用就是他的地址,存入到int当中,此时的 vft就是存储的地址,如何用这个地址去访问其中的函数即可.上面我们知道,在虚表的结束位置,会设置为0.所以我们可以以此来判断

运行结果:

image-20230905161534648

在上面的函数中,只有fun1和fun3被Student完成了重写.

所以我们可以得出一个结论:

在单继承模型中,派生类会复制一份基类的虚表到自己中,若有重写函数,则用新的重写函数地址覆盖原函数地址 而不是直接对基类虚表直接进行修改,自己新的虚函数则跟在后面

6.2多继承中的虚函数表

class Person{
public:
	virtual void fun1()
	{
		cout << "Person::fun1";
	}
	virtual void fun2()
	{
		cout << "Person::fun2";
	}
	virtual void fun3()
	{
		cout << "Person::fun3" ;
	}
};
class People {
public:
	virtual void fun1()
	{
		cout << "People::fun1";
	}
	virtual void fun2()
	{
		cout << "People::fun2";
	}
	virtual void fun3()
	{
		cout << "People::fun3";
	}
};

class Student :public Person,public People
{
	virtual void fun1() {
		cout << "Student::fun1()";
	}
	
	virtual void fun4()
	{
		cout << "Student::fun4()";
	}
};
typedef void(*FUNC_PTR)();
void printvft(FUNC_PTR* table)
{
	for (int i = 0; table[i] != nullptr; i++)
	{
		printf("%d->%p", i, table[i]);
		FUNC_PTR f=table[i];
		f();
		cout << endl;
	} 
}
int main()
{
	Student s1;
	Person p1;
	People peo;
	cout << "person:" << endl;
	int vft = *((int*)&p1);
	printvft((FUNC_PTR*)vft);
	cout << "people:" << endl;
	vft = *((int*)&peo);
	printvft((FUNC_PTR*)vft);
	cout << "student:" << endl;
	vft = *((int*)&s1);
	printvft((FUNC_PTR*)vft);
}

运行结果:

image-20230905163221242

我们在内存中看一下模型:

student中的person:

image-20230905163302648

student中的people:

image-20230905163312561

我们可以很容易发现,student是对person进行了重写,并把自己的未重写虚函数放在了第一张虚表的最后.

也就是:多继承模型中,派生类会在第一个继承的基类上进行重写,并且将自己未重写的虚函数放在其表尾
image-20230905164632777

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【C++技能树】多态解析 的相关文章

  • File.AppendText 尝试写入错误的位置

    我有一个 C 控制台应用程序 它作为 Windows 任务计划程序中的计划任务运行 此控制台应用程序写入日志文件 该日志文件在调试模式下运行时会创建并写入应用程序文件夹本身内的文件 但是 当它在任务计划程序中运行时 它会抛出一个错误 指出访
  • 类型约束

    我有以下类层次结构 class Header IEnumerable
  • 为什么从字典中获取时会得到 Action<> 的克隆?

    我有以下字典 private Dictionary
  • 在视口中查找 WPF 控件

    Updated 这可能是一个简单或复杂的问题 但在 wpf 中 我有一个列表框 我用一个填充数据模板从列表中 有没有办法找出特定的数据模板项位于视口中 即我已滚动到其位置并且可以查看 目前我连接到了 listbox ScrollChange
  • 将 JSON 参数从 java 发布到 sinatra 服务

    我有一个 Android 应用程序发布到我的 sinatra 服务 早些时候 我无法读取 sinatra 服务上的参数 但是 在我将内容类型设置为 x www form urlencoded 之后 我能够看到参数 但不完全是我想要的 我在
  • 在mysql连接字符串中添加应用程序名称/程序名称[关闭]

    Closed 这个问题需要细节或清晰度 help closed questions 目前不接受答案 我正在寻找一种解决方案 在连接字符串中添加应用程序名称或程序名称 以便它在 MySQL Workbench 中的 客户端连接 下可见 SQL
  • 当单元格内的 JComboBox 中有 ItemEvent 时,如何获取 CellRow

    我有一个 JTable 其中有一列包含 JComboBox 我有一个附加到 JComboBox 的 ItemListener 它会根据任何更改进行操作 但是 ItemListener 没有获取更改的 ComboBox 所在行的方法 当组合框
  • 保护 APK 中的字符串

    我正在使用 Xamarin 的 Mono for Android 开发一个 Android 应用程序 我目前正在努力使用 Google Play API 添加应用内购买功能 为此 我需要从我的应用程序内向 Google 发送公共许可证密钥
  • Keycloak - 自定义 SPI 未出现在列表中

    我为我的 keycloak 服务器制作了一个自定义 SPI 现在我必须在管理控制台上配置它 我将 SPI 添加为模块 并手动安装 因此我将其放在 module package name main 中 并包含 module xml 我还将其放
  • 高效列出目录中的所有子目录

    请参阅迄今为止所采取的建议的编辑 我正在尝试使用 WinAPI 和 C 列出给定目录中的所有目录 文件夹 现在我的算法又慢又低效 使用 FindFirstFileEx 打开我正在搜索的文件夹 然后我查看目录中的每个文件 使用 FindNex
  • 等待 IAsyncResult 函数直至完成

    我需要创建等待 IAsyncResult 方法完成的机制 我怎样才能做到这一点 IAsyncResult result contactGroupServices BeginDeleteContact contactToRemove Uri
  • 如何配置eclipse以保持这种代码格式?

    以下代码来自 playframework 2 0 的示例 Display the dashboard public static Result index return ok dashboard render Project findInv
  • Springs 元素“beans”不能具有字符 [children],因为该类型的内容类型是仅元素

    我在 stackoverflow 中搜索了一些页面来解决这个问题 确实遵循了一些正确的答案 但不起作用 我是春天的新人 对不起 这是我的调度程序 servlet
  • WebBrowser.Print() 等待完成。 。网

    我在 VB NET 中使用 WebBrowser 控件并调用 Print 方法 我正在使用 PDF 打印机进行打印 当调用 Print 时 它不会立即启动 它会等到完成整个子或块的运行代码 我需要确保我正在打印的文件也完整并继续处理该文件
  • 在 Windows Phone silverlight 8.1 上接收 WNS 推送通知

    我有 Windows Phone 8 1 silverlight 应用程序 我想使用新框架 WNS 接收通知 我在 package appxmanifest 中有
  • 休眠以持久保存日期

    有没有办法告诉 Hibernate java util Date 应该持久保存 我需要这个来解决 MySQL 中缺少的毫秒分辨率问题 您能想到这种方法有什么缺点吗 您可以自己创建字段long 或者使用自定义的UserType 实施后User
  • KeyPressed 和 KeyTyped 混淆[重复]

    这个问题在这里已经有答案了 我搜索过之间的区别KeyPressedand KeyTyped事件 但我仍然不清楚 我发现的一件事是 Keypressed 比 KeyTyped 首先被触发 请澄清一下这些事件何时被准确触发 哪个适合用于哪个目的
  • GCC 的“-Wl,option”和“-Xlinker option”语法之间有区别吗?

    我一直在查看一些配置文件 并且看到它们都被使用 尽管在不同的体系结构上 如果您在 Linux 机器上使用 GCC 将选项传递给链接器的两种语法之间有区别吗 据我所知 阅读 GCC 手册时 他们的解释几乎相同 From man gcc Xli
  • 如何减少具有多个单元的 PdfPTable 的内存消耗

    我正在使用 ITextSharp 创建一个 PDF 它由单个 PdfTable 组成 不幸的是 对于特定的数据集 由于创建了大量 PdfPCell 我遇到了内存不足异常 我已经分析了内存使用情况 我有近百万个单元格的 1 2 在这种情况下有
  • 灵气序列解析问题

    我在使用 Spirit Qi 2 4 编写解析器时遇到一些问题 我有一系列键值对以以下格式解析

随机推荐

  • Base64编码(汇编版,未做过多优化,性能自认为还可以)

    感谢 DelphiGuy 于 2010 10 08 17 27 37 给出的提醒 function GetSizeCoder3To4 InputCount Integer Integer inline begin Result InputC
  • 本地映射到外网

    很多人做开发的苦恼 外网访问不了本地 很多调试进行不了 比如说微信开发 这个时候要用手机调试 但是服务器在自己电脑上 外网访问不了 这个时候我们可以用一些工具 使我们的内网ip映射到外网 让外网可以访问 一 使用ngrok让微信公众平台通过
  • POST请求常见错误及解决办法

    POST请求常见错误及解决办法 前后端分离 已经是web开发的主流 在前后端对接的过程中难免会碰到各式各样的问题 本文对近期项目中遇到的与 POST请求 有关的问题做了一个简要的汇总和分析 并列出了与之相关的解决办法 问题一 POST请求发
  • 区块链之java调用智能合约(二)部署智能合约

    前言 上一节 已经说过 如何的创建一个合约 如何编译合约 然后在java中调用 但是呢 这些还远远不够 那么还差哪些呢 现在就是如何将创建的智能合约部署的对应的共链 私链 测试链中了 需要部署后 才能真正的使用 现在就讲讲如何部署智能合约
  • linux下gcc的使用教程,Linux下GCC使用方法简介

    编译 第一步 是进行预编译 使用 E参数可以让GCC在预处理结束后停止编译过程 gcc E hello c o hello i 预处理的宏定义插入到hello i中 第二步 是将hello i编译为目标代码 这可以通过使用 c参数来完成 g
  • C++使用string的大数运算(6)模加模减模乘模幂

    本次项目目标 使用C 完成对于大数的相关运算 项目要点 1 大数指的是远超long long int的数据 2 将大数用矩阵进行存储 并通过矩阵实现运算 3 本人采用字符串进行存储 应注意char的特点 比如 char a 161 cout
  • 数据挖掘:银行评分卡制作——数据分箱、WOE、IV的意义

    在银行评分卡的项目中 通常都会需要把数据分箱 分箱后并不是对数据进行哑变量处理 而是用WOE值去替换 再放入模型中 学习的过程中会对这些操作有些疑问 比如 数据分箱有什么意义 WOE和IV值是干什么的 这里对这些数据处理的意义进行一个说明
  • [OpenAirInterface实战-6] :OAI在github中源代码的存放结构

    作者主页 文火冰糖的硅基工坊 文火冰糖 王文兵 的博客 文火冰糖的硅基工坊 CSDN博客 本文网址 OpenAirInterface实战 6 OAI在github中源代码的存放结构 文火冰糖 王文兵 的博客 CSDN博客 目录 第1章 基本
  • Zookeeper的常见面试题

    1 Zookeeper 1 1 Zookeeper基本概念 Zookeeper作为一个优秀高效且可靠的分布式协调框架 ZooKeeper 在解决分布式数据一致性问题时并没有直接使用Paxos算法 而是专门定制了一致性协议叫做 ZAB Zoo
  • 芯片低功耗设计原则

    原则1 按照自顶向下 从架构级到门级 的方法 在不同设计层次上对功耗进行优化 设计层次越高 优化所能达到的效果越好 原则2 从系统级着眼全局 从细节处精打细算 从产品解决方案角度而不局限于芯片本身来进行功耗预算 并根据功耗的分析结果 优化系
  • xxl-job任务详解

    文章目录 任务管理 新增任务页面字段释义 1 1 路由策略 1 2 运行模式 BEAN模式 GLUE模式 1 3 阻塞处理策略 1 4 子任务ID 1 5 JobHandler 1 6 Cron 1 7 任务超时时间 任务操作 任务管理 新
  • labview与matlab接口,LabVIEW Comms与MATLAB®的互联接口

    为了复用现有的MATLAB 代码 LabVIEW Communications System Design Suite LabVIEW Comms 新增了MATLAB专用接口的功能 无线原型的开发者可使用已有的MATLAB函数或脚本 将其连
  • 图片占位符在线工具

    简介 在前端的开发中 有时需要加载各种尺寸的图片 而在开发阶段 这些真实的图片可能并未制作完成 因此 我们可以使用图片占位符工具生成假的图片进行替代 本文介绍一款十分灵活的图片占位符工具 您只需在调用API服务时修改图片尺寸参数就可以生成不
  • 机器学习入门教学——可解释性

    1 前言 近年来 机器学习模型被广泛地应用到现实生活中的一些重要领域 如面部识别 自动驾驶 语言处理和智慧医疗等 然而 机器学习模型就像一个黑盒子 给予一个输入 就能得到一个决策结果 但是我们并不知道模型是如何做决策的 因此 可解释性旨在帮
  • Struts2标签与jsp页面Java代码的值相互使用

    业务需求 Struts标签使用Jsp页面中的list的值 java代码使用Struts传来的值
  • 从ChatGPT与New Bing看程序员为什么要学习算法?

    文章目录 为什么要学习数据结构和算法 ChatGPT与NEW Bing 的回答 想要通关大厂面试 就不能让数据结构和算法拖了后腿 业务开发工程师 你真的愿意做一辈子CRUD boy吗 对编程还有追求 不想被行业淘汰 那就不要只会写凑合能用的
  • 51单片机PCA模块配置

    PCA模块是 可编程计数器阵列 的缩写 英文名称是 Programmable Counter Array 以下的说明均以SILICON LAB生产的C8051系列微型控制器为例 PCA包括1个16位 定时器 计数器 和5个 捕获 比较模块
  • 动态规划算法(背包问题)

    1 应用场景 背包问题 背包问题 有一个背包 容量为4磅 现有如下物品 1 要求达到的目标为装入的背包的总价值最大 并且重量不超出 2 要求装入的物品不能重复 2 动态规划算法介绍 1 动态规划 Dynamic Programming 算法
  • Python datetime模块中strptime和strftime的区别和使用方法

    strptime和strftime是两个比较容易搞混的函数 今天恰好用到 在这里做一个总结 strptime p表示parse 表示分析的意思 所以strptime是给定一个时间字符串和分析模式 返回一个时间对象 strftime f表示f
  • 【C++技能树】多态解析

    Halo 这里是Ppeua 平时主要更新C 数据结构算法 Linux与ROS 感兴趣就关注我bua 文章目录 0 多态的概念 0 1 多态的定义 1 重写 2 Final与Override 3 抽象类 4 多态中的内存分布 4 1虚表存在哪