【C++】菱形继承为何会引发二义性?虚继承如何破解?

【C++】菱形继承为何会引发二义性?虚继承如何破解?



🎬 个人主页MSTcheng · ZEEKLOG
🌱 代码仓库MSTcheng · Gitee
🔥 精选专栏: 《C语言
数据结构
C++由浅入深

💬座右铭:路虽远行则将至,事虽难做则必成!


前言:在之前的文章中,我们已经介绍了继承的相关内容,但有些重要概念尚未涉及,例如菱形继承、虚继承以及二义性等问题。本文将重点探讨这些内容。加粗样式

文章目录

一、多继承及菱形继承问题

1.1单继承

单继承:⼀个派⽣类只有⼀个直接基类时称这个继承关系为单继承。

第一种情形:

在这里插入图片描述
#include<iostream>#include<string>usingnamespace std;classPerson{public:voidprint(){ cout <<"姓名:"<< _name << endl; cout <<"年龄:"<< _age << endl;} string _name="张三";int _age=18;};classTeacher:publicPerson{public: string _subject;//科目int _id;//职工编号voidprintTeacher(){ cout <<"姓名:"<< _name << endl; cout <<"年龄:"<< _age << endl; cout <<"职工编号:"<< _id << endl;}};classStudent:publicTeacher{public:int _num;//学号voidprintStudent(){ cout <<"姓名:"<< _name << endl; cout <<"年龄:"<< _age << endl; cout <<"学号:"<< _num << endl; cout <<"科目:"<< _subject << endl;}};intmain(){ Student s; s._name ="李四";//继承自person s._age =19;//继承自person s._num =241601;//继承自person s._subject ="数学";//继承自Teacher s.printStudent();return0;}
在这里插入图片描述

1.2多继承

多继承:⼀个派⽣类两个或以上直接基类时称这个继承关系为多继承,多继承对象在内存中的模型是,先继承的基类在前⾯后⾯继承的基类在后⾯派⽣类成员在放到最后⾯。如下图所示:

在这里插入图片描述


当我们在为学生和教师设计一个共同的基类"Person"时,这种继承结构就会形成菱形继承关系,因其类关系图呈现菱形所以叫菱形继承。 如下图:

在这里插入图片描述


代码示例:

classPerson{public: string _name;// 姓名};classStudent:publicPerson{protected:int _num;//学号};classTeacher:publicPerson{protected:int _id;// 职工编号};//Assistant继承了两个类classAssistant:publicStudent,publicTeacher{protected: string _majorCourse;// 主修课程};intmain(){// 编译报错:error C2385: 对“_name”的访问不明确 Assistant a;//a._name = "peter";// 需要显示指定访问哪个基类的成员可以解决二义性问题,但是数据冗余问题无法解决 a.Student::_name ="xxx"; a.Teacher::_name ="yyy";return0;}
在这里插入图片描述
通过上面的运行结果我们发现,菱形继承存在两个问题:(1)二义性问题;(2)数据冗余问题。为什么会产生二义性问题?
其实是因为 Anssistant继承了两个类 ,而这两个类各自都包含了一份Person类,所以在访问Anssistant_name时编译器不知道是访问TeacherPerson类中的_name还是StudentPerson类中的_name。这就导致了_name的访问不明确。
解决二义性的办法
:指定类域(是Teacher类的Person还是Student类的Person)数据冗余问题很好理解,就是Assistant中包含了两个Person类的信息,正常情况下应该只包含一份,但是这里包含两份所以冗余。
解决冗余的办法:虚继承!

1.3虚继承

有了多继承就可能有菱形继承,而菱形继承又存在二义性和数据冗余等问题。所以C++就引入了虚继承来解决数据的冗余问题。但是菱形虚拟继承它的底层实现比较复杂,性能也会有损失,所以最好不要设计菱形继承。

菱形虚拟继承通过在中间类声明中添加virtual关键字实现,下面来看代码

classPerson{public: string _name;// 姓名};classStudent:vritualpublicPerson{protected:int _num;//学号};classTeacher:virtualpublicPerson{protected:int _id;// 职工编号};//Assistant继承了两个类classAssistant:publicStudent,publicTeacher{protected: string _majorCourse;// 主修课程};intmain(){ Assistant a; a._name ="peter";//这时候再去访问就不会报错了return0;}
在这里插入图片描述

采用虚继承的方式相当于将TeacherStudent类中的Person部分提取到一个公共空间。因此,通过Assistant对象a访问的_name成员,能够明确指向Person类中的_name。这也解决了数据的冗余性问题

在这里插入图片描述

1.3.1为什么通过虚继承可以将Person部分成员提取出来?

菱形虚拟继承的原理:

虚继承通过共享基类实例实现成员提取。当使用虚继承时,派生类会包含一个指向共享基类实例的指针(虚基类指针),而非直接嵌入基类成员。这使得不同路径继承的虚基类在最终派生类中指向同一内存地址

 Person / \ vptr vptr / \ Teacher Student \ / vptr vptr \ / Anssistant 
虚继承通过虚基类表(vbtable)实现偏移量管理。 每个包含虚基类的派生类都会生成虚基类表,记录虚基类相对于该派生类起始地址的偏移量。这使得无论通过哪条继承路径访问,都能定位到同一个基类实例。

内存布局:

Anssistant对象内存布局: +-------------------+ | Teacher成员数据 | +-------------------+ | Student成员数据 | +-------------------+ | Anssistant新数据 | +-------------------+ | Person共享数据 | <-- 所有vptr指向此处 +-------------------+ 

注意:

  • Person只存在一份
  • 每个虚继承的中间类(Teacher、Student)内部不直接包含Person成员(被提取出来了)而是包含一个指向最后Person共享数据处的指针。
  • Anssistant被实例化时,最终在最后生成一个Person共享数据。
  • 所有通过虚继承的类访问Person时,都会通过指针+偏移量访问到Person的共享数据区,从而访问Person的成员。

1.3.2虚继承的构造初始化顺序

classPerson{public:Person(constchar* name):_name(name){} string _name;// 姓名};classStudent:virtualpublicPerson{public:Student(constchar* name,int num):Person(name),_num(num){}protected:int _num;//学号};classTeacher:virtualpublicPerson{public:Teacher(constchar* name,int id):Person(name),_id(id){}protected:int _id;// 职⼯编号};classAssistant:publicStudent,publicTeacher{public:Assistant(constchar* name1,constchar* name2,constchar* name3):Person(name3),Student(name1,1),Teacher(name2,2){}protected: string _majorCourse;// 主修课程};intmain(){//这⾥a对象中_name是"张三", "李四", "王五"中的哪⼀个? Assistant a("张三","李四","王五");return0;}

虚继承的构造初始化顺序:

  • 虚基类优先初始化
    Assistant的构造函数中,Person(name3)显式调用了虚基类的构造函数,传入name3("王五")。此时_name被初始化为"王五"
  • 非虚基类初始化
    Student(name1, 1)和Teacher(name2, 2)的构造函数中也会调用Person的构造函数,但由于Person是虚基类且已初始化,这些调用会被忽略。

注意:
虚继承场景下,最派生类必须直接初始化虚基类,且虚基类的初始化优先于其他基类。因此_name的值由Assistant构造函数中显式指定的Person(name3)决定。

二、总结

共享基类子对象虚继承确保在多重继承层次结构中,派生类只包含一个共享的基类子对象。

虚基类指针或引用编译器会为虚继承的类生成额外的信息(如虚基类表或偏移量),用于在运行时定位共享基类子对象。这通常通过虚基类指针(vptr)或间接寻址实现。

初始化责任转移虚基类的初始化由最派生类(Most Derived Class)直接完成,而非中间派生类。这与普通继承不同,普通继承中每个中间派生类都需要初始化其直接基类。

内存布局调整虚继承可能导致类的内存布局发生变化,通常会增加额外的间接层或偏移量信息,以支持共享基类的访问。这会带来一定的运行时开销。

构造函数调用顺序虚基类的构造函数会在任何非虚基类之前被调用,确保共享基类子对象优先初始化。这一顺序由语言标准严格规定。
MSTcheng 始终坚持用直观图解+实战代码,把复杂技术拆解得明明白白!
👁️ 【关注】 看普通程序员如何用实用派思路搞定复杂需求
👍【点赞】 给 “不搞虚的” 技术分享多份认可
🔖 【收藏】 把这些 “好用又好懂” 的干货技巧存进你的知识库
💬 【评论】 来唠唠 —— 你踩过最 “离谱” 的技术坑是啥?
🔄 【转发】把实用技术干货分享给身边有需要的程序员伙伴
技术从无唯一解,让我们一起用最接地气的方式,写出最扎实的代码! 🚀💻

Read more

【Spring Boot开发实战手册】掌握Springboot开发技巧和窍门(十三)前端匹配界面、后端匹配WebSocket

【Spring Boot开发实战手册】掌握Springboot开发技巧和窍门(十三)前端匹配界面、后端匹配WebSocket

前言 在现代 Web 开发中,前端和后端的协作变得越来越重要,特别是在需要实时交互和数据更新的应用场景中。WebSocket 技术作为一种全双工通信协议,使得前端和后端之间的实时数据传输变得更加高效和稳定。本篇博客将会探讨如何设计和实现一个实时匹配系统,其中前端负责展示用户界面并与后端进行交互,而后端则通过 WebSocket 协议来处理数据通信。 前端 onMounted: 当组件被挂载的时候执行的函数 onUnmonted: 当组件被卸载的时候执行的函数 初步调试阶段,我们是将token传进user.id的 store/pk.js: import ModuleUser from'./user'exportdefault{state:{socket:null,//ws链接opponent_username:"",opponent_photo:"",status:"matching",//matching表示匹配界面,playing表示对战界面},getters:

By Ne0inhk
【测试理论与实践】(十)Web 项目自动化测试实战:从 0 到 1 搭建博客系统 UI 自动化框架

【测试理论与实践】(十)Web 项目自动化测试实战:从 0 到 1 搭建博客系统 UI 自动化框架

目录 前言 一、项目背景与测试规划:先明确 "测什么" 和 "怎么测" 1.1 项目介绍 1.2 测试目标 1.3 测试范围与用例设计 编辑 二、环境搭建:3 步搞定自动化测试前置准备 2.1 安装核心依赖包 2.2 浏览器配置 2.3 项目目录结构设计 三、核心模块开发:封装公共工具,提高代码复用性 3.1 驱动管理与截图工具封装(common/Utils.py) 3.2 代码说明与优化点 四、测试用例开发:

By Ne0inhk
深入剖析云原生Service Mesh数据平面Envoy核心架构:基于xDS协议与WebAssembly实现动态流量管理与安全策略的微服务治理实战指南

深入剖析云原生Service Mesh数据平面Envoy核心架构:基于xDS协议与WebAssembly实现动态流量管理与安全策略的微服务治理实战指南

深入剖析云原生Service Mesh数据平面Envoy核心架构:基于xDS协议与WebAssembly实现动态流量管理与安全策略的微服务治理实战指南 在云原生微服务架构的演进中,Service Mesh(服务网格)已成为处理服务间通信的标准基础设施。而在这一架构中,Envoy 凭借其高性能的 C++ 实现、可扩展的架构以及作为 Istio 默认数据平面的地位,成为了事实上的“Sidecar之王”。 本文将深入剖析 Envoy 的核心架构,重点解析其如何通过 xDS 协议 实现动态配置,以及如何利用 WebAssembly (Wasm) 技术突破传统的扩展瓶颈,实现微服务的流量管理与安全策略治理。 1. Envoy 核心架构全景:高性能的“四层”模型 Envoy 本质上是一个高性能的边缘/服务代理,其设计核心在于将网络处理逻辑分解为清晰的层级。这种设计不仅保证了极高的吞吐量,也使得配置极其灵活。 1.1 逻辑架构分层 Envoy 的逻辑架构自上而下分为四个核心层次: Level 1: 线程模型与I/

By Ne0inhk
网站检测不用等! Web-Check+cpolar让异地协作查漏洞更高效

网站检测不用等! Web-Check+cpolar让异地协作查漏洞更高效

文章目录 * 前言 * 1.关于Web-Check * 2.功能特点 * 3.安装Docker * 4.创建并启动Web-Check容器 * 5.本地访问测试 * 6.公网远程访问本地Web-Check * 7.内网穿透工具安装 * 8.创建远程连接公网地址 * 9.使用固定公网地址远程访问 前言 Web-Check 是一款全方位的网站诊断工具,能检测 IP 信息、SSL 证书、DNS 记录、开放端口等关键数据,适合开发者做性能优化、运维人员做安全巡检,还能帮安全测试人员识别潜在风险。它的优点是结果可视化强,所有数据在仪表盘分类呈现,不用手动整合多工具报告,省时又清晰。 用 Web-Check 时发现,检测前最好确认目标网站能正常访问,否则可能出现数据不全;另外,生成的报告里有不少专业术语,新手可以先查基础概念(比如 SSL 链、DNS

By Ne0inhk