【C++】继承和多态扩展学习

目录

1. 菱形虚拟继承原理剖析

1.1.虚基表

2. 单继承和多继承的虚函数表深入探索

2.1 单继承虚函数表深入探索

2.2 多继承虚函数表深入探索

​编辑

2.3 菱形继承、菱形虚拟继承

3. 继承和多态考察的一些常见问题


1. 菱形虚拟继承原理剖析

继承的文章中我们讲到C++的多继承就会引发一些场景出现菱形继承,有了菱形继承,就会出现数据冗余和二义性的问题,C++又引入了虚继承来解决数据冗余和二义性。

class Person
{
public:string _name; // 姓名
};
// class Student : public Person
class Student : virtual public Person
{
protected:int _num; // 学号
};
// class Teacher : public Person
class Teacher : virtual public Person
{
protected:int _id; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected:string _majorCourse; // 主修课程
};
void Test()
{// 这样会有二义性无法明确知道访问的是哪一个Assistant a;a._name = "peter";// 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决a.Student::_name = "xxx";a.Teacher::_name = "yyy";
}

• 为了研究虚拟继承原理,我们给出了一个简化的菱形继承继承体系,再借助内存窗口观察对象成员的模型。要注意的是这里必须借助内存窗口才能看到真实的底层对象内存模型,vs编译器的监视窗口是经过特殊处理的,以它的角度给出了一个方便看的样子,但并不是本来的样子。但是有时想看清真实的内存模型,往往需要借助内存窗口。

1.1.虚基表

在前面继承的文章中,我们了解到为了避免菱形继承所导致的数据冗余,子类会将重复继承的部分合并为一份,放在类的最上或者最下面。但是这里引出一个问题是当我们通过父类指针访问子类对象,这是对于合并的部分,要如何确定位置呢?大家可能觉得合并的部分不是已经放在最后或者最上面了吗?但是这里如果我们使用不同的父类指针,偏移多少才能到底呢?因此需要虚基表记录父类对应的偏移量。

虚基表是编译器为了解决多重继承场景下的菱形继承问题所设计的,虚基表(vbtable)通过记录虚基类实例的偏移量来指示派生类如何访问唯一的虚基类实例。当子类通过多继承方式继承多个具有共同基类的父类时,如果不使用虚继承,子类会包含多分共同基类的数据,这会导致数据冗余。而是要虚继承,子类中只会包含一份共同基类的数据。

• 通过下面的简化菱形虚拟继承模型,我们可以看到,D对象中的B和C部分中分别包含一个指向虚基表的指针,B指向的虚基表中存储了B对象部分距离公共的A的相对偏移量距离,C指向的虚基表中存储了C对象部分距离公共的A的相对偏移量距离。这样公共的虚基类A部分在D对象中就只有一份了,这样就解决了数据冗余和二义性的问题。


• 通过B的对象模型,我们发现菱形虚拟继承中B和C的对象模型跟D保持的一致的方式去存储管理A,这样当B的这指针访问A时,无论B指针切片指向D对象,还是B指针直接指向B对象,访问A成员都是通过虚基表指针的方式查找到A成员再访问。

class A
{
public:int _a;
};
// class B : public A
class B : virtual public A
{
public:int _b;
};
// class C : public A
class C : virtual public A
{
public:int _c;
};
class D : public B, public C
{
public:int _d;
};
int main()
{D d;d.B::_a = 1;d.C::_a = 2;d._a = 3;d._b = 4;d._c = 5;d._d = 6;return 0;
}
int main()
{D d;d.B::_a = 1;d.C::_a = 2;d._a = 3;d._b = 4;d._c = 5;d._d = 6;B b;b._a = 7;b._b = 8;// B的指针指向B对象B *p2 = &b;// B的指针指向D对象切片B *p1 = &d;// p1和p2分别对指向的_a成员访问修改// 分析内存模型,我们发现B对象也使用了虚基表指向A成员的模型// 所以打开汇编我们看到下面的访问_a的方式是一样的p1->_a++;p2->_a++;return 0;
}

2. 单继承和多继承的虚函数表深入探索

2.1 单继承虚函数表深入探索

• vs编译器的监视窗口是经过特殊处理的,以它的角度给出了一个方便看的样子,但并不是本来的样子。多态部分我们讲了,虚函数指针都要放进虚函数表,这里我们通过监视窗口观察Derive对象,看不到func3和func4在虚表中,借助内存窗口可以看到一个地址,但是并不确认是不是func3和func4的地址。所以下面我们写了一份特殊代码,通过指针的方式,强制访问了虚函数表,调用了虚函数,确认继承中虚函数表中的真实内容。

class Base
{
public:virtual void func1() { cout << "Base::func1" << endl; }virtual void func2() { cout << "Base::func2" << endl; }private:int a;
};
class Derive : public Base
{
public:virtual void func1() { cout << "Derive::func1" << endl; }virtual void func3() { cout << "Derive::func3" << endl; }virtual void func4() { cout << "Derive::func4" << endl; }void func5() { cout << "Derive::func5" << endl; }private:int b;
};
typedef void (*VFPTR)();
void PrintVTable(VFPTR vTable[])
{// 依次取虚表中的虚函数指针打印并调用。调用就可以看出存的是哪个函数cout << " 虚表地址>" << vTable << endl;// 注意如果是在g++下面,这里就不能用nullptr去判断访问虚表结束了for (int i = 0; vTable[i] != nullptr; ++i){printf(" 第%d个虚函数地址 :0X%p,->", i, vTable[i]);VFPTR f = vTable[i];f();}cout << endl;
}
int main()
{Base b;Derive d;// 32位程序的访问思路如下:// 需要注意的是如果是在64位下,指针是8byte,对应程序位置就需要进行更改// 思路:取出b、d对象的头4bytes,就是虚表的指针,前面我们说了虚函数表本质是一个存虚函数指针的指针数组,vs下这个数组最后面放了一个nullptr,g++ 下面最后没有nullptr// 1.先取b的地址,强转成一个int*的指针// 2.再解引用取值,就取到了b对象头4bytes的值,这个值就是指向虚表的指针// 3.再强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。// 4.虚表指针传递给PrintVTable进行打印虚表// 5.需要说明的是这个打印虚表的代码经常会崩溃,因为编译器有时对虚表的处理不干净,虚表最后面没有放nullptr,导致越界,这是编译器的问题。我们只需要点目录栏的 -生成 - 清理解决方 案,再编译就好了。 VFPTR *vTable1 = (VFPTR *)(*(int *)&b);PrintVTable(vTable1);VFPTR *vTable2 = (VFPTR *)(*(int *)&d);PrintVTable(vTable2);return 0;
}

2.2 多继承虚函数表深入探索

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;
};
typedef void (*VFPTR)();
void PrintVTable(VFPTR vTable[])
{cout << " 虚表地址>" << vTable << endl;for (int i = 0; vTable[i] != nullptr; ++i){printf(" 第%d个虚函数地址 :0X%p,->", i, vTable[i]);VFPTR f = vTable[i];f();}cout << endl;
}
int main()
{Derive d;VFPTR *vTableb1 = (VFPTR *)(*(int *)&d);PrintVTable(vTableb1);VFPTR *vTableb2 = (VFPTR *)(*(int *)((char *)&d + sizeof(Base1)));PrintVTable(vTableb2);Base1 *p1 = &d;p1->func1();Base2 *p2 = &d;p2->func1();d.func1();return 0;
}

• 跟前面单继承类似,多继承时Derive对象的虚表在监视窗口也观察不到部分虚函数的指针。所以我们一样可以借助上面的思路强制打印虚函数表。


• 需要注意的是多继承时,Derive中同时继承了Base1和Base2,内存中先继承的对象在前面,并且Derive中包含的Base1和Base2各有一张虚函数表,通过观察我们发现Derive没有重写的虚函数func3,选择放在先继承的Base1的虚函数表中。


• 另外需要注意的是,有些细心的读者发现Derive对象中重写的Base1虚表的func1地址和重写Base2虚表的func1地址不一样,这是为什么呢?这个问题还比较复杂。需要我们分别对这两个函数进行多态调用,并翻阅对应的汇编代码进行分析,才能捋清楚问题所在。这里简单说一个结论就是本质Base2虚表中func1的地址并不是真实的func1的地址,而是封装过的func1地址,因为Base2指针p2指向Derive时,Base2部分在中间位置,切片时,指针会发生偏移,那么多态调用p2->func1()时,p2传递给this前需要把p2给修正回去指向Derive对象,因为func1是Derive重写的,里面this应该是指向Derive对象的

2.3 菱形继承、菱形虚拟继承

实际中我们不建议设计出菱形继承及菱形虚拟继承,一方面太复杂容易出问题,另一方面这样的模
型,访问基类成员有一定得性能损耗。所以菱形继承、菱形虚拟继承我们的虚表本文就不看了,一般我们也不需要研究清楚,因为实际中很少用。好奇心强的读者,可以去看下面的两篇链接文章。

1. C++ 虚函数表解析
2. C++ 对象的内存布局

class A
{
public:virtual void func1() {}public:int _a;
};
class B : virtual public A
{
public:virtual void func1() {}virtual void func2() {}public:int _b;
};
class C : virtual public A
{
public:virtual void func1() {}virtual void func3() {}public:int _c;
};
class D : public B, public C
{
public:D(): _d(1){}inline virtual void func1() {}virtual void func4() {}public:int _d;
};
int main()
{D d;d.B::_a = 1;d.C::_a = 2;d._a = 3;d._b = 4;d._c = 5;d._d = 6;return 0;
}

3. 继承和多态考察的一些常见问题

1. 什么是多态?答:参考前面多态文章


2. 什么是重载、重写(覆盖)、重定义(隐藏)?答:参考前面多态文章


3. 多态的实现原理?答:参考前面多态文章


4. inline函数可以是虚函数吗?答:可以,不过编译器就忽略inline属性,这个函数就不再是inline属性,因为虚函数要放到虚表中去,也就是说inline属性和虚函数属性是不同同时存在的。


5. 静态成员可以是虚函数吗?答:不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。


6. 构造函数可以是虚函数吗?答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。


7. 析构函数可以是虚函数吗?什么场景下析构函数是虚函数?答:可以,并且最好把基类的析构函数定义成虚函数。参考本文内容


8. 对象访问普通函数快还是虚函数更快?答:首先如果是普通对象调用,是一样快的。如果是指针或者是引用对去调用,则调用的普通函数快,因为构成多态,运行时调用虚函数需要到虚函数表中去查找。


9. 虚函数表是在什么阶段生成的,存在哪的?答:虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。


10. C++菱形继承的问题?虚继承的原理?答:参考前面继承文章。注意这里不要把虚函数表和虚基表搞混了。


11. 什么是抽象类?抽象类的作用?答:参考前面继承文章;抽象类强制重写了虚函数,另外抽象类体现出了接口继承关系。

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

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

相关文章

Visual Studio Code 远端云服务器开发使用指南

目录 一、下载安装 1、官方下载 2、下载加速方案 二、基于Ubuntu系统的开发环境搭建方案 1、开发环境配置 2、云服务器架构 3、工作流程关系 4、总结 三、推荐插件 1、免配置插件 1. Remote-SSH - 远程登录Linux服务器 2. C/C - 必备的C/C开发插件 3. C/C Extensi…

技术演进中的开发沉思-41 MFC系列:定制 AppWizard

MFC开发&#xff0c;最为重要的无非就是用“MFC AppWizard” 对话框做开发了&#xff0c;第一次使用感觉像拆收音机的孩子 —— 左边是项目类型选择&#xff0c;右边是一堆打勾的选项&#xff0c;点完 “完成”&#xff0c;屏幕上就冒出了能直接编译运行的窗口程序。那时还不知…

Libevent(3)之使用教程(2)创建事件

Libevent(3)之使用教程(2)创建事件 Author: Once Day Date: 2025年6月29日 一位热衷于Linux学习和开发的菜鸟&#xff0c;试图谱写一场冒险之旅&#xff0c;也许终点只是一场白日梦… 漫漫长路&#xff0c;有人对你微笑过嘛… 本文档翻译于&#xff1a;Fast portable non-bl…

Kotlin 作用域函数 let 的实现原理

Kotlin 中的 let 是一个 标准库扩展函数&#xff0c;它广泛用于作用域函数&#xff08;Scope Functions&#xff09;中&#xff0c;尤其适用于对可空对象&#xff08;nullable&#xff09;做非空判断并执行代码块的场景。 示例代码 val name: String? "123" name?…

从FDTD仿真到光学神经网络:机器学习在光子器件设计中的前沿应用工坊

FDTD仿真与光学神经网络的基础概念 FDTD&#xff08;时域有限差分&#xff09;是一种数值方法&#xff0c;用于求解麦克斯韦方程组&#xff0c;广泛应用于光子器件设计。光学神经网络通过光波导、衍射元件等物理结构实现矩阵运算&#xff0c;具有低能耗、高并行的优势。 机器学…

在Ubutu22系统上面离线安装Go语言环境【教程】

0.引言 Go语言&#xff08;又称Golang&#xff09;是Google开发的一种静态强类型、编译型、并发型编程语言&#xff0c;由Robert Griesemer、Rob Pike和Ken Thompson于2007年开始设计&#xff0c;2009年正式发布。 1.到官网下载压缩包 2.从win10系统离线上传压缩包给ubuntu22…

CMake实践:CMake3.30版本之前和之后链接boost的方式差异

目录 1.背景 2.boost引入CMake时机 3.CMake 3.30 之前&#xff08;含 3.29&#xff09;链接 Boost 的方式 4.CMake 3.30 及之后链接 Boost 的方式 5.CMake3.30后引入Boost的步骤 6.迁移建议&#xff08;3.30 之前 → 3.30 之后&#xff09; 7.CMake 3.30 移除FindBoost的…

告别挂马风险!PBootCMS完美替代方案BadouCMS

开发企业网站时一直比较喜欢用pbootcms,标签套用很简单&#xff0c;使用也方便。 但是pbootcms一直有被挂马的问题&#xff0c;官方好像也不怎么更新了&#xff01;换过好几个cms&#xff0c;比如eyoucms、dedecms、帝国等等&#xff0c;感觉都不怎么能用得习惯&#xff0c;还…

开发者如何集成AI绘画?智创聚合API简化Midjourney接入

在 AI 绘画领域&#xff0c;Midjourney 的大名如雷贯耳&#xff0c;其强大的图像生成能力&#xff0c;能将我们脑海中的奇思妙想&#xff0c;迅速转化为精美的视觉画面&#xff0c;深受设计师、艺术家以及广大创意爱好者的青睐。然而&#xff0c;使用 Midjourney 的过程中&…

pycharm回车、删除、方向键和快捷键等不能使用原因

解决方法 &#xff1a;菜单栏中的Tools取消勾选Vim Emulator 原因 &#xff1a;新版的pycharm安装中&#xff0c;默认安装了vim扩展&#xff0c;一旦安装了pycharm在编写代码时会默认使用Vim编辑器

修复ffmpeg.dll丢失错误|6种解决ffmpeg.dll方法详细教程

看到电脑提示“ffmpeg.dll丢失”&#xff0c;很多人会懵。ffmpeg.dll 是个处理视频、音频的关键文件。它要是没了或坏了&#xff0c;软件就打不开或直接闪退。常见原因是软件安装不全、文件被删、或者中病毒。下面说说它是干嘛的&#xff0c;再给解决办法。一.ffmpeg.dll 到底是…

OkHttp 与 Stetho 结合使用:打造强大的 Android 网络调试工具链

前言在 Android 应用开发过程中&#xff0c;网络请求的调试一直是一个重要但具有挑战性的环节。Facebook 开发的 Stetho 是一个强大的调试工具&#xff0c;当它与 OkHttp 结合使用时&#xff0c;可以为我们提供前所未有的网络请求洞察能力。本文将详细介绍如何将这两者结合使用…

LangGraph教程10:LangGraph ReAct应用

文章目录 ReAct 预构建的代理 向 ReAct 代理添加记忆 向 ReAct 代理添加系统提示 向 ReAct 代理添加人机交互 ReAct 官方文档地址:https://langchain-ai.github.io/langgraph/how-tos/#prebuilt-react-agent 中文文档地址:https://www.aidoczh.com/langgraph/how-tos/#react…

安卓第一个项目

测试所有摄像头 安卓CameraX&#xff1a;https://developer.android.com/media/grow/spatial-audio?hlzh-cn 1、MainActivity.java // 定义包名 package com.mms.densenapplication;// 引入 AppCompatActivity&#xff0c;支持兼容性更强的 Activity import androidx.appcompa…

Google Gemini 体验

文章中代码仓库 gemini 谷歌推出的 AI 只能模型 Gemini官网Gemini ChatGemini开发者文档Gemini SDK 所有模型 模型变体输入输出优化目标Gemini 2.5 Pro gemini-2.5-pro音频、图片、视频、文本和 PDF文本增强的思考和推理能力、多模态理解能力、高级编码能力等Gemini 2.5 Fla…

Trae安装指定版本的插件

前情 Trae是属于国产的跟 Cursor类似的AI编程IDE&#xff0c;我也是第一时间体验Trae的&#xff0c;虽然相比Cursor弱了一些&#xff0c;但是也绝对胜任了&#xff0c;前端因为排队问题我转战了Cursor&#xff0c;等到Trae出收费模式前&#xff0c;我已经办了Cursor会员了&…

【技术追踪】用于医学图像合成和分割的噪声一致孪生扩散模型(CVPR-2025)

孪生扩散模型&#xff0c;生成息肉图像用于提升分割性能&#xff01; 论文&#xff1a;Noise-Consistent Siamese-Diffusion for Medical Image Synthesis and Segmentation 代码&#xff1a;https://github.com/Qiukunpeng/Siamese-Diffusion 0、摘要 深度学习已彻底革新医学影…

Crontab详解

crontab是Unix/Linux系统中用于设置周期性任务的工具&#xff0c;通过编辑配置文件实现定时执行命令或脚本。以下是其语法规则和核心要点&#xff1a; 一、基本格式 * * * * * command - - - - - | | | | | | | | | ----…

中国1km逐月潜在蒸散发数据集 - matlab按shp批量裁剪

中国1km逐月潜在蒸散发数据集 - matlab按shp批量裁剪 1. 数据概述 2 利用掩膜文件对数据进行裁剪 3 完整代码 4 结语 本篇继续处理气象数据,中国1km逐月潜在蒸散发数据集同前节介绍的中国1km降水数据集一样,都可以从国家青藏高原科学数据中心获得,数据具有同样的空间分辨率(…

Node.js链接MySql

前言&#xff1a; 在现代 Web 开发和后端服务中&#xff0c;Node.js 因其高性能和异步特性被广泛使用。MySQL 作为流行的关系型数据库之一&#xff0c;提供了稳定高效的数据存储和管理能力。将 Node.js 与 MySQL 结合&#xff0c;可以构建强大的数据驱动型应用。 一、环境准备…