进阶了解C++(4)——多态

       在上篇文章中,简单的介绍了多态中的概念以及其相关原理。本文将针对多态中其他的概念进一步进行介绍,并且更加深入的介绍关于多态的相关原理。

目录

1. 抽象类:

2. 再谈虚表:

3. 多继承中的虚函数表:


1. 抽象类:

       在上篇文章中提到了,如果使用关键字virtual修饰一个成员函数,则这个成员函数被称为虚函数。此处,针对虚函数进行扩展,如果在虚函数的声明后面加上=0,则这个函数被称为纯虚函数。包含纯虚函数的类又叫抽象类,其特点是不能初始化出对象。即使是子类继承这个类,同样也不能初始化出对象。只有认为对纯虚函数进行重写,才能初始化出一个对象。

    给定一个抽象类及其子类如下:

//抽象类
class Person
{
public:virtual void func() = 0{cout << "Person-func()";}
};class Teacher : public Person
{
public:};class Student : public Person
{
public:};

如果向初始化出这三个类的对象,即:
 

int main()
{Person p;Student s;Teacher t;
}

此时编译器报错如下:

如果对子类中继承父类中的纯虚函数进行重写,即:

class Teacher : public Person
{
public:virtual void func(){cout << "Teacher-func()" << endl;}
};class Student : public Person
{
public:virtual void func(){cout << "Student-func()" << endl;}
};

此时再去分别初始化两个子类的对象,即:

int main()
{Student s;s.func();Teacher t;t.func();
}

代码可以正常运行,且运行结果如下:

2. 再谈虚表:

在之前C++基础的文章中提到了,在构造函数中,存在初始化列表,初始化列表初始化成员变量的顺序并不是根据初始化列表的顺序,而是根据成员变量声明的顺序。对于虚函数,其也符合这个特性。具体可以用下面的代码进行证明:

class Base
{
public:virtual void Func1(){cout << "Base::Func1()" << endl;}virtual void Func2(){cout << "Base::Func2()" << endl;}void Func3(){cout << "Base::Func3()" << endl;}
private:int _b = 1;
};
class Derive : public Base
{
public:virtual void Func1(){cout << "Derive::Func1()" << endl;}
private:int _d = 2;
};
int main()
{Base b;Derive d;return 0;
}

通过监视窗口,查看对象b中虚表:

       可以看到,虚函数在虚表中存放的顺序,正是虚函数在类中声明的顺序。对于这一点,也同样可以在内存窗口中进行查看。

       

 从图中不难发现,对象b中的第一个地址,恰好对应了虚表指针的地址。此时再查看虚表中的内容,即:

不难看出,再内存窗口中,第二,第三条地址分别对应了虚表中两个虚函数的地址。

而对于子类,其生成的对象d中的内容如下:

对于子类对象的内容,可以分为两个部分,一是从父类中继承的内容,二是子类中自己的成员变量以及函数。在监视窗口中,可以看到子类继承了父类的虚表,并且对其中进行重写的虚函数的地址进行了覆盖。但是需要注意,在子类中,并不存在自己的虚表 。对于子类虚表中的函数指针如下:
在上面给出的图片中可以看出,蓝线连接的两个地址分别是父类、子类中的虚函数Func1(),但是因为这个函数在子类中发生了重写,因此,父类,子类中这两个虚函数的地址并不相同。

而对于紫线连接的两个虚函数,由于虚函数并未在子类中发生虚函数的重写,因此,父类,子类中俩个虚函数的地址相同。

如果对于子类,再添加一个虚函数,例如:

class Derive : public Base
{
public:virtual void Func1(){cout << "Derive::Func1()" << endl;}virtual void Func(){;}
private:int _d = 2;
};

 此时,在监视窗口中进行查看,子类对象的虚表中并没有出现新的虚函数的函数指针,但是在内存窗口中,却出现了一条新的地址,对于这个新的地址,一般认为就是子类中新加入的虚函数。至于具体的验证,将在文章后面给出。 

 

(注:为了方便演示,下面的代码在x86,即32位环境下运行)

在之前C++基础关于内存管理的文章中(C++(9)——内存管理-CSDN博客 )提到了系统根据不同的需求,将内存划分为不同的部分,具体如下:

1.栈:用于存储非全局、非静态的局部变量,函数参数,返回值等等

2.堆:用于程序运行时的内存的动态开辟

3.数据段(静态区):用于存储全局变量和静态变量

4.代码段(常量区):可执行代码\只读常量

在给出了上述概念后,文章将探讨一个 问题,即:虚表指针是存储在什么地方的。

为了方便测试,首先给出上面四个类型变量的地址,即:

int i = 1;//栈int* p = new int;//堆static int j = 0;//数据段(静态区)const char* p2 = "xxxxxxx";//代码段(常量区)printf("栈=%p\n", &i);printf("堆=%p\n", p);printf("静态区=%p\n", &j);printf("常量区=%p\n",p2);

打印结果如下:

对于如何获取虚表指针,本文提供一种方法:由于虚表指针存储在一个类的前四个字节,因此,只需要初始化出一个该类的对象,首先获取这个对象的指针,在将这个指针强转成int*类型,即可获取虚表指针,具体代码如下:

Base* B = &b;Derive* D = &d;printf("B=%p\n", *(int*)B);printf("D=%p\n", *(int*)D);

打印结果如下:

从上述区段以及两个虚表指针的指针对比来看,虚表指针应该存储在常量区,也就是代码段。

上面给出了如何获取虚表指针的存储地址,下面给出虚表中,如何获取虚表中存储各个虚函数的指针,具体方法如下:

typedef void(*VF_PTR)();
void PrintVF(VF_PTR* vf)
{for (size_t i = 0; vf[i] != nullptr; i++){printf("[%d] :%p", i, vf[i]);}
}
PrintVF((VF_PTR*) * (int*)&d);

打印结果如下:

如果在获取了上述指针后,直接调用这些函数指针,便可知道上述 获取的地址是否是类中的虚函数,即:
 

typedef void(*VF_PTR)();
void PrintVF(VF_PTR* vf)
{for (size_t i = 0; vf[i] != nullptr; i++){printf("[%d] :%p", i, vf[i]);VF_PTR f = vf[i];f();}
}

打印结果如下:

通过这个例子可以看出,虽然在上面添加新的虚函数Func3()时,在子类的虚表中并没有看到这个函数的地址,但是在次数,照样可以通过函数指针调用这个函数,这也间接证明了Func3()其实添加到了子类中,只是在监视窗口不可见。

3. 多继承中的虚函数表:

给定代码如下:

class Base1 {
public:virtual void func1() { cout << "Base1::func1" << endl; }virtual void func2() { cout << "Base1::func2" << endl; }
private:int b1;
};
class Base2 {
public:virtual void func1() { cout << "Base2::func1" << endl; }virtual void func2() { cout << "Base2::func2" << endl; }
private:int b2;
};
class Derive : public Base1, public Base2 {
public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }
private:int d1;
};int main()
{Derive d;Base1* p1 = &d;Base2* p2 = &d;return 0;
}

在上面给出的代码中,存在三个类,其中Base1,Base2被集成到了Derive中,由于Base1最先被继承到子类中,因此,可以认为,父类成员在子类的空间中的位置是最靠前的。对于&d表示取对象d的首地址,由于父类成员在空间中位置是最靠前的,因此,理论上p1==&d。而对于p2,由于其在Base1后继承,因此p2相对于p1是靠后的,因此,在子类中,存在着两张虚表,这两个虚表分别有着自己独立的地址。在监视窗口中,同样可以证明这一点:

而对于Derive中的虚函数func3(),为了验证func3()是存储在哪个虚表中的,可以用下面的代码进行检验:

PrintVF((VF_PTR*)*(int*)p1);

对于Base1中虚表中存储的函数指针打印结果如下:

下面打印Base2中虚表中的函数指针:

由此证明,子类中的虚函数func3()是存储在子类继承并且进行覆盖的Base1中的虚表。

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

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

相关文章

MySQL 用户账号迁移

文章目录 前言1. 工具安装1.1 下载安装包1.2 编译安装 2. 用户迁移后记 前言 有一个典型的使用场景&#xff0c;就是 RDS 下云大多数都是通过 DTS 进行数据传输的&#xff0c;用户是不会同步到自建数据库的。需要运维人员在自建数据库重新创建用户&#xff0c;如果用户数量很多…

基于springboot+vue的在线考试与学习交流平台

博主主页&#xff1a;猫头鹰源码 博主简介&#xff1a;Java领域优质创作者、CSDN博客专家、阿里云专家博主、公司架构师、全网粉丝5万、专注Java技术领域和毕业设计项目实战&#xff0c;欢迎高校老师\讲师\同行交流合作 ​主要内容&#xff1a;毕业设计(Javaweb项目|小程序|Pyt…

中小型水库安全监测运营解决方案,筑牢水库安全防线

我国水库大坝具有“六多”的特点。第一&#xff0c;总量多。我国现有水库9.8万座&#xff0c;是世界上水库大坝最多的国家。第二&#xff0c;小水库多。我国现有水库中95%的水库是小型水库。第三&#xff0c;病险水库多。 目前&#xff0c;在我国水库管理中&#xff0c;部分地方…

供应链|NUS覃含章MS论文解读:数据驱动下联合定价和库存控制的近似方法 (二)

编者按 本次解读的文章发表于 Management Science&#xff0c;原文信息&#xff1a;Hanzhang Qin, David Simchi-Levi, Li Wang (2022) Data-Driven Approximation Schemes for Joint Pricing and Inventory Control Models. https://doi.org/10.1287/mnsc.2021.4212 文章在数…

深度神经网络联结主义的本质

一、介绍 在新兴的人工智能 (AI) 领域&#xff0c;深度神经网络 (DNN) 是一项里程碑式的成就&#xff0c;突破了机器学习、模式识别和认知模拟的界限。这一技术奇迹的核心是一个与认知科学本身一样古老的思想&#xff1a;联结主义。本文深入探讨了联结主义的基本原理&#xff0…

实例:NX二次开发抽取平面以及标准柱面中心线

一、概述 最近体验许多外挂&#xff0c;包括胡波外挂、星空外挂及模圣等都有抽取面的中心线&#xff0c;由于刚刚学习&#xff0c;我尝试看看能不能做出来&#xff0c;本博客代码没有封装函数&#xff0c;代码有待改进&#xff0c;但基本可以实现相应的功能。 二、案例实现的功…

【web APIs】3、(学习笔记)有案例!

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、概念其他事件页面加载事件元素滚动事件页面尺寸事件 元素尺寸与位置 二、案例举例电梯导航 前言 掌握阻止事件冒泡的方法理解事件委托的实现原理 一、概念…

SpringCloud Alibaba(保姆级入门及操作)

第一章 微服务概念 1.0 科普一些术语 科普一下项目开发过程中常出现的术语,方便后续内容的理解。 **服务器:**分软件与硬件,软件:类型tomcat这种跑项目的程序, 硬件:用来部署项目的电脑(一般性能比个人电脑好) **服务:**操作系统上术语:一个程序,开发中术语:一个…

数学建模【分类模型】

一、分类模型简介 本篇将介绍分类模型。对于二分类模型&#xff0c;我们将介绍逻辑回归&#xff08;logistic regression&#xff09;和Fisher线性判别分析两种分类算法&#xff1b;对于多分类模型&#xff0c;我们将简单介绍SPSS中的多分类线性判别分析和多分类逻辑回归。 分…

Java面试题之并发

并发 1.并发编程的优缺点&#xff1f;2.并发编程三要素&#xff1f;3.什么叫指令重排&#xff1f;4.如何避免指令重排&#xff1f;5.并发&#xff1f;并行&#xff1f;串行&#xff1f;6.线程和进程的概念和区别&#xff1f;7.什么是上下文切换&#xff1f;8.守护线程和用户线程…

<网络安全>《60 概念讲解<第七课 网络模型OSI对应协议>》

1 OSI模型 OSI模型&#xff08;Open Systems Interconnection Model&#xff09;是一个由国际标准化组织&#xff08;ISO&#xff09;提出的概念模型&#xff0c;用于描述和标准化电信或计算系统的通信功能&#xff0c;以实现不同通信系统之间的互操作性。该模型将通信系统划分…

【k8s管理--Helm包管理器】

1、Helm的概念 Kubernetes包管器 Helm是查找、分享和使用软件构件Kubernetes的最优方式。 Helm管理名为chart的Kubernetes包的工具。Helm可以做以下的事情&#xff1a; 从头开始创建新的chat将chart打包成归档tgz)文件与存储chat的仓库进行交互在现有的Kubernetes集群中安装和…

【Android】View 的滑动

View 的滑动是 Android 实现自定义控件的基础&#xff0c;同时在开发中我们也难免会遇到 View 的滑动处理。其实不管是哪种滑动方式&#xff0c;其基本思想都是类似的&#xff1a;当点击事件传到 View 时&#xff0c;系统记下触摸点的坐标&#xff0c;手指移动时系统记下移动后…

【AI+应用】怎么快速制作一个类chatGPT套壳网站

最近有人问我&#xff0c; 看了我之前写的一篇文章 [人工智能] AI浪潮下Sora对于普通人的机会 &#xff0c; 怎么做一个类chatGPT的套壳网站&#xff0c;是从0开始做么。 对于普通人来说&#xff0c;万事不懂先AI&#xff0c; AI找不到答案搜索google或百度。对于程序员来说…

C# 获取类型 Type.GetType()

背景 C#是强类型语言&#xff0c;任何对象都有Type&#xff0c;有时候需要使用Type来进行反射、序列化、筛选等&#xff0c;获取Type有Type.GetType, typeof()&#xff0c;object.GetType() 等方法&#xff0c;本文重点介绍Type.GetType()。 系统类型/本程序集内的类型 对于系…

有哪些视频媒体?邀请视频媒体报道活动的好处

传媒如春雨&#xff0c;润物细无声&#xff0c;大家好&#xff0c;我是51媒体网胡老师。 视频媒体在当今的媒体生态中占据了重要的地位。以下是一些主要的视频媒体类型&#xff1a; 电视台&#xff1a;如中央电视台、各省级卫视台、地方电视台等&#xff0c;他们拥有专业的视…

探索Redis 6.0的新特性

Redis&#xff08;Remote Dictionary Server&#xff09;是一个开源的内存中数据结构存储系统&#xff0c;通常被用作缓存、消息队列和实时数据处理等场景。它的简单性、高性能以及丰富的数据结构支持使其成为了众多开发者和企业的首选。在Redis 6.0版本中&#xff0c;引入了一…

【深蓝学院】移动机器人运动规划--第7章 集群机器人运动规划--笔记

文章目录 0. Contents1. Multi-Agent Path Finding (MAPF)1.1 HCA*1.2 Single-Agent A*1.3 ID1.4 M*1.5 Conflict-Based Search(CBS)1.6 ECBS1.6.1 heuristics1.6.2 Focal Search 2. Velocity Obstacle (VO&#xff0c;速度障碍物)2.1 VO2.2. RVO2.3 ORCA 3. Flocking model&am…

ChatGPT4.0 的优势、升级 4.0 为什么这么难以及如何进行升级?

前言 “ChatGPT4.0一个月多少人民币&#xff1f;” ”chatgpt4账号“ ”chatgpt4 价格“ “chatgpt4多少钱” 最近发现很多小伙伴很想知道关于ChatGPT4.0的事情&#xff0c;于是写了这篇帖子&#xff0c;帮大家分析一下。 一、ChatGPT4.0 的优势 &#xff08;PS&#xff1a;…

LINUX基础培训二十七之shell标准输入、输出、错误

一、Shell 输入/输出重定向 大多数 UNIX 系统命令从你的终端接受输入并将所产生的输出发送回​​到您的终端。一个命令通常从一个叫标准输入的地方读取输入&#xff0c;默认情况下&#xff0c;这恰好是你的终端。同样&#xff0c;一个命令通常将其输出写入到标准输出&#xff…