【C++】继承深度解析:继承方式和菱形虚拟继承的详解

【C++】继承深度解析:继承方式和菱形虚拟继承的详解
在这里插入图片描述
✨ 坚持用清晰易懂的图解+代码语言, 让每个知识点都简单直观!
🚀 个人主页不呆头 · ZEEKLOG
🌱 代码仓库不呆头 · Gitee
📌 专栏系列 :📖 《C语言》🧩 《数据结构》💡 《C++》🐧 《Linux》💬 座右铭 :“不患无位,患所以立。”

【C++】继承深度解析:继承方式和菱形虚拟继承的详解


摘要

本文系统讲解了 C++ 中的继承机制,包括单继承、多继承、菱形继承及虚拟继承,配合图解和代码示例直观展示各类继承的对象模型、内存布局及访问特点,重点说明虚拟继承如何通过共享基类实例、虚基表偏移和构造顺序,解决菱形继承的数据冗余与二义性问题。
继承的认识和基础学习详解----------》请点击


目录

一、继承方式

1. 单继承

一个派生类只有一个直接基类的时候称这个继承为单继承
Person

Teacher

Student
(单链)
在这里插入图片描述
// 基类:人classPerson{public: string name;int age;voidShowPerson(){ cout <<"姓名: "<< name <<",年龄: "<< age << endl;}};// 派生类:老师(继承人)classTeacher:publicPerson{public: string subject;voidShowTeacher(){ cout <<"姓名: "<< name <<",年龄: "<< age <<",教授科目: "<< subject << endl;}};// 派生类:学生(继承老师)classStudent:publicTeacher{public: string school;voidShowStudent(){ cout <<"姓名: "<< name <<",年龄: "<< age <<",教授科目: "<< subject <<",学校: "<< school << endl;}};intmain(){ Student s; s.name ="呆头";// 来自 Person s.age =20;// 来自 Person s.subject ="C++";// 来自 Teacher s.school ="野鸡大学";// 来自 Student s.ShowStudent();return0;}
运行结果
多链
// 基类:人classPerson{public: string name;int age;voidShowPerson(){ cout <<"姓名: "<< name <<",年龄: "<< age << endl;}};// 派生类:老师classTeacher:publicPerson{public: string subject;voidTeach(){ cout << name <<" 正在教授 "<< subject <<" 课程。"<< endl;}};// 派生类:学生classStudent:publicPerson{public: string school;voidStudy(){ cout << name <<" 在 "<< school <<" 学习。"<< endl;}};intmain(){ Teacher t; t.name ="张老师"; t.age =35; t.subject ="C++"; t.ShowPerson(); t.Teach(); cout <<"------------------"<< endl; Student s; s.name ="李华"; s.age =20; s.school ="宁夏大学"; s.ShowPerson(); s.Study();return0;}

2. 多继承

一个派生类有两个或者以上的直接基类时称这个继承关系为多继承,多继承对象在内存中的模型是先继承的基类在前面,后继承的基类在后面,派生类成员放在最后面。多继承的使用方法是在子类的位置对多个父类使用逗号,进行间隔,其余方式public形式不变,进行继承
 Person Teacher Athlete \ | / \ | / ----> Student 
// 基类:人classPerson{public: string name;int age;};// 老师类classTeacher{public: string subject;voidTeach(){ cout <<"正在教授 "<< subject <<" 课程。"<< endl;}};// 运动员类classAthlete{public: string sport;voidTrain(){ cout <<"正在训练 "<< sport <<" 项目。"<< endl;}};// 学生类(多继承)classStudent:publicPerson,publicTeacher,publicAthlete{public: string school;voidShow(){ cout <<"姓名: "<< name <<",年龄: "<< age <<",学校: "<< school <<",教授科目: "<< subject <<",运动项目: "<< sport << endl;}};intmain(){ Student s; s.name ="李华"; s.age =20; s.school ="宁夏大学"; s.subject ="C++"; s.sport ="800米"; s.Show(); s.Teach(); s.Train();return0;}
运行结果

3. 菱形继承

菱形继承是多继承的⼀种特殊情况。菱形继承的问题有数据冗余和⼆义性的问题。⽀持多继承就⼀定会有菱形继承,像Java就直接不⽀持多继承,规避掉了这⾥的问题,所以实践中我们也是不建议设计出菱形继承这样的模型的。
在这里插入图片描述
#include<iostream>#include<string>usingnamespace std;// 基类:人classPerson{public: string name;int age;};// 老师继承人classTeacher:publicPerson{public: string subject;voidTeach(){ cout << name <<" 正在教授 "<< subject << endl;}};// 运动员继承人classAthlete:publicPerson{public: string sport;voidTrain(){ cout << name <<" 正在训练项目:"<< sport << endl;}};// 学生同时继承老师与运动员classStudent:publicTeacher,publicAthlete{public: string school;voidShow(){// ⚠️ 错误:编译器不知道 name 是哪个基类的// cout << "姓名: " << name << endl; // ❌ 二义性 cout <<"Teacher::name = "<< Teacher::name << endl; cout <<"Athlete::name = "<< Athlete::name << endl; cout <<"学校: "<< school << endl;}};intmain(){ Student s; s.Teacher::name ="张老师"; s.Athlete::name ="李同学";// 各自一份 name s.subject ="C++"; s.sport ="800米"; s.school ="宁夏大学"; s.Show();// 访问需要显式指定路径 s.Teacher::Teach(); s.Athlete::Train();return0;}
Student
├── Teacher::Person
│ ├── name
│ └── age
├── Athlete::Person
│ ├── name
│ └── age
└── school


3.1 菱形虚拟继承
如何解决菱形继承带来的弊端?——》使用虚拟继承!以上述对象模型为例,在 Teacher 和 Athlete 继承 Person 时采用虚拟继承,即可避免 Student 同时拥有两份 Person 子对象。虚拟继承是一种专门用于解决菱形继承问题的机制,在其它普通继承场景下不建议使用。在 C++ 中,虚拟继承的使用方式是:当多个派生类继承自同一个公共基类,并且这些派生类又被同一个子类多继承时,应当在这些“中间层”父类继承公共基类的地方,使用 virtual 关键字进行修饰。(上述示例就应该在老师和运动处使用virtual进行修饰)
classPerson{public: string name;int age;};// 老师虚继承人classTeacher:virtualpublicPerson{public: string subject;};// 运动员虚继承人classAthlete:virtualpublicPerson{public: string sport;};// 学生多继承classStudent:publicTeacher,publicAthlete{public: string school;voidShow(){ cout <<"姓名: "<< name << endl;// ✅ 不再二义性 cout <<"学校: "<< school << endl;}};
3.2 菱形虚拟继承的原理

回顾我们上述示例是这样的继承关系:

 Person / \ Teacher Athlete \ / Student 
如果Student同时继承Teacher和Athlete,他就会有两份Person成员(一次从Teacher一次从Athlete),这样会造成数据冗余。调用Student的Person的成员函数或者访问成员变量的时候,编译器就不知道选择Teacher中的还是Athlete中的那一份,就造成了二义性

3.2.1 内存布局

虚拟继承会让共享的基类在内存中只有一份,并且派生类通过 虚基表(vbt/vptr类似) 来找到这份共享的基类。

Student 对象布局(虚继承): [ Student 部分 ][ Teacher 部分 ][ Athlete 部分 ][ Person(共享) ] 

注意 Person 只存在一份。

  • 每个通过虚继承的中间类(TeacherAthlete)内部不直接包含 Person 成员,而是 包含一个指向共享 Person 的指针(实际上是偏移量)。
  • Student 被实例化时,最终会在对象末尾放置一份 Person 数据。
  • 所有通过虚继承的类访问 Person 时,通过偏移量找到唯一的 Person

示意图:

Student 内存: [ Student fields ] [ Teacher fields | pointer_to_shared_Person ] [ Athlete fields | pointer_to_shared_Person ] [ Shared Person fields ] 

3.2.2 虚表(vtable/vptr)调整

虚继承通常涉及两个表:

普通虚表:用于多态函数调用。
虚基表(VBT):用于找到虚继承基类的偏移量。
访问虚基类成员时:编译器生成代码不是 this + offset_of_Person_in_Teacher。而是 this + vbt_offset → 找到唯一 Person

举例:

Student s; s.Teacher::Person::name ="Alice";// 编译器查找 Teacher 的虚基表 -> 定位到 Student 中唯一的 Person

底层逻辑
thisStudent 的起始地址 → 查找虚基表中的 Person 偏移量 → 访问共享 Person


3.2.3 构造函数调用顺序
虚继承的构造顺序也不同:最顶层虚基类(Person)先构造。然后是非虚中间类(TeacherAthlete)。最后是最派生类(Student)。

这保证了 虚基类只构造一次,防止重复初始化。


总结底层逻辑
虚拟继承的本质可以理解为:共享基类唯一实例 → 解决数据冗余。虚基表存偏移量 → 动态定位虚基类成员,解决二义性。构造顺序调整 → 确保虚基类只构造一次。对象访问成本略高 → 需要间接查找偏移量,但换来安全和唯一性。

换句话说,虚拟继承底层就是:

“中间类不直接存虚基类,而是存一个指向虚基类偏移的指针;派生类实例化时才分配唯一的虚基类存储。”
在这里插入图片描述

总结

C++ 继承方式灵活多样,单继承简单直观,多继承可组合功能但可能出现二义性,而菱形继承会导致数据冗余和访问冲突;虚拟继承通过共享基类实例和虚基表机制有效解决菱形继承问题,同时调整构造顺序保证基类唯一初始化,是处理复杂继承关系的关键技术。


不是呆头将一直坚持用清晰易懂的图解+代码语言,让每个知识点变得简单!
👁️ 【关注】 看一个非典型程序员如何用野路子解决正经问题
👍 【点赞】 给“不写八股文”的技术分享一点鼓励
🔖 【收藏】 把这些“奇怪但有用”的代码技巧打包带走
💬 【评论】 来聊聊——你遇到过最“呆头”的 Bug 是啥?
🗳️ 【投票】 您的投票是支持我前行的动力
技术没有标准答案,让我们一起用最有趣的方式,写出最靠谱的代码! 🎮💻

Read more

【OpenClaw从入门到精通】第10篇:OpenClaw生产环境部署全攻略:性能优化+安全加固+监控运维(2026实测版)

【OpenClaw从入门到精通】第10篇:OpenClaw生产环境部署全攻略:性能优化+安全加固+监控运维(2026实测版)

摘要:本文聚焦OpenClaw从测试环境走向生产环境的核心痛点,围绕“性能优化、安全加固、监控运维”三大维度展开实操讲解。先明确生产环境硬件/系统选型标准,再通过硬件层资源管控、模型调度策略、缓存优化等手段提升响应速度(实测响应效率提升50%+);接着从网络、权限、数据三层构建安全防护体系,集成火山引擎安全方案拦截高危操作;最后落地TenacitOS可视化监控与Prometheus告警体系,配套完整故障排查清单和虚拟实战案例。全文所有配置、代码均经实测验证,兼顾新手入门实操性和进阶读者的生产级部署需求,帮助开发者真正实现OpenClaw从“能用”到“放心用”的跨越。 优质专栏欢迎订阅! 【DeepSeek深度应用】【Python高阶开发:AI自动化与数据工程实战】【YOLOv11工业级实战】 【机器视觉:C# + HALCON】【大模型微调实战:平民级微调技术全解】 【人工智能之深度学习】【AI 赋能:Python 人工智能应用实战】【数字孪生与仿真技术实战指南】 【AI工程化落地与YOLOv8/v9实战】【C#工业上位机高级应用:高并发通信+性能优化】 【Java生产级避坑指南:

By Ne0inhk
ARM Linux 驱动开发篇--- Linux 并发与竞争实验(互斥体实现 LED 设备互斥访问)--- Ubuntu20.04互斥体实验

ARM Linux 驱动开发篇--- Linux 并发与竞争实验(互斥体实现 LED 设备互斥访问)--- Ubuntu20.04互斥体实验

🎬 渡水无言:个人主页渡水无言 ❄专栏传送门: 《linux专栏》《嵌入式linux驱动开发》《linux系统移植专栏》 ❄专栏传送门: 《freertos专栏》《STM32 HAL库专栏》 ⭐️流水不争先,争的是滔滔不绝  📚博主简介:第二十届中国研究生电子设计竞赛全国二等奖 |国家奖学金 | 省级三好学生 | 省级优秀毕业生获得者 | ZEEKLOG新星杯TOP18 | 半导纵横专栏博主 | 211在读研究生 在这里主要分享自己学习的linux嵌入式领域知识;有分享错误或者不足的地方欢迎大佬指导,也欢迎各位大佬互相三连 目录 前言  一、实验基础说明 1.1、互斥体简介 1.2 本次实验设计思路 二、硬件原理分析(看过之前博客的可以忽略) 三、实验程序编写 3.1 互斥体 LED 驱动代码(mutex.c) 3.2.1、设备结构体定义(28-39

By Ne0inhk
Flutter for OpenHarmony:swagger_dart_code_generator 接口代码自动化生成的救星(OpenAPI/Swagger) 深度解析与鸿蒙适配指南

Flutter for OpenHarmony:swagger_dart_code_generator 接口代码自动化生成的救星(OpenAPI/Swagger) 深度解析与鸿蒙适配指南

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net 前言 后端工程师扔给你一个 Swagger (OpenAPI) 文档地址,你会怎么做? 1. 对着文档,手写 Dart Model 类(容易写错字段类型)。 2. 手写 Retrofit/Dio 的 API 接口定义(容易拼错 URL)。 3. 当后端修改了字段名,你对着报错修半天。 这是重复劳动的地狱。 swagger_dart_code_generator 可以将 Swagger (JSON/YAML) 文件直接转换为高质量的 Dart 代码,包括: * Model 类:支持 json_serializable,带 fromJson/

By Ne0inhk
Linux 开发别再卡壳!makefile/git/gdb 全流程实操 + 作业解析,新手看完直接用----《Hello Linux!》(5)

Linux 开发别再卡壳!makefile/git/gdb 全流程实操 + 作业解析,新手看完直接用----《Hello Linux!》(5)

文章目录 * 前言 * make/makefile * 文件的三个时间 * Linux第一个小程序-进度条 * 回车和换行 * 缓冲区 * 程序的代码展示 * git指令 * 关于gitee * Linux调试器-gdb使用 * 作业部分 前言 做 Linux 开发时,你是不是也遇到过这些 “卡脖子” 时刻?写 makefile 时,明明语法没错却报错,最后发现是依赖方法行没加 Tab;想提交代码到 gitee,记不清 git add/commit/push 的 “三板斧”,还得反复搜教程;用 gdb 调试程序,输了命令没反应,才想起编译时没加-g生成 debug 版本;甚至连写个进度条,都搞不懂\r和\n的区别,导致进度条乱跳…… 其实这些问题,

By Ne0inhk