当前位置: 首页 > news >正文

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

在这里插入图片描述

✨ 坚持用 清晰易懂的图解 + 代码语言, 让每个知识点都 简单直观
🚀 个人主页 :不呆头 · CSDN
🌱 代码仓库 :不呆头 · Gitee
📌 专栏系列

  • 📖 《C语言》
  • 🧩 《数据结构》
  • 💡 《C++》
  • 🐧 《Linux》

💬 座右铭“不患无位,患所以立。”


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

  • 摘要
  • 目录
    • 一、继承方式
      • 1. 单继承
      • 2. 多继承
      • 3. 菱形继承
        • 3.1 菱形虚拟继承
        • 3.2 菱形虚拟继承的原理
        • 3.2.1 内存布局
        • 3.2.2 虚表(vtable/vptr)调整
        • 3.2.3 构造函数调用顺序
        • 总结底层逻辑
  • 总结


摘要

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


目录

一、继承方式

1. 单继承

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

Teacher

Student
(单链)

在这里插入图片描述

// 基类:人
class Person
{
public:string name;int age;void ShowPerson(){cout << "姓名: " << name << ",年龄: " << age << endl;}
};// 派生类:老师(继承人)
class Teacher : public Person
{
public:string subject;void ShowTeacher(){cout << "姓名: " << name<< ",年龄: " << age<< ",教授科目: " << subject << endl;}
};// 派生类:学生(继承老师)
class Student : public Teacher
{
public:string school;void ShowStudent(){cout << "姓名: " << name<< ",年龄: " << age<< ",教授科目: " << subject<< ",学校: " << school << endl;}
};int main()
{Student s;s.name = "呆头";       // 来自 Persons.age = 20;            // 来自 Persons.subject = "C++";     // 来自 Teachers.school = "野鸡大学";  // 来自 Students.ShowStudent();return 0;
}

运行结果
在这里插入图片描述

多链
在这里插入图片描述

// 基类:人
class Person
{
public:string name;int age;void ShowPerson(){cout << "姓名: " << name << ",年龄: " << age << endl;}
};// 派生类:老师
class Teacher : public Person
{
public:string subject;void Teach(){cout << name << " 正在教授 " << subject << " 课程。" << endl;}
};// 派生类:学生
class Student : public Person
{
public:string school;void Study(){cout << name << " 在 " << school << " 学习。" << endl;}
};int main()
{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();return 0;
}

2. 多继承

一个派生类有两个或者以上的直接基类时称这个继承关系为多继承,多继承对象在内存中的模型是先继承的基类在前面,后继承的基类在后面,派生类成员放在最后面。

多继承的使用方法是在子类的位置对多个父类使用逗号,进行间隔,其余方式public形式不变,进行继承

 Person       Teacher       Athlete\          |          /\         |         /---->  Student
// 基类:人
class Person
{
public:string name;int age;
};// 老师类
class Teacher
{
public:string subject;void Teach() { cout << "正在教授 " << subject << " 课程。" << endl; }
};// 运动员类
class Athlete
{
public:string sport;void Train() { cout << "正在训练 " << sport << " 项目。" << endl; }
};// 学生类(多继承)
class Student : public Person, public Teacher, public Athlete
{
public:string school;void Show(){cout << "姓名: " << name<< ",年龄: " << age<< ",学校: " << school<< ",教授科目: " << subject<< ",运动项目: " << sport << endl;}
};int main()
{Student s;s.name = "李华";s.age = 20;s.school = "宁夏大学";s.subject = "C++";s.sport = "800米";s.Show();s.Teach();s.Train();return 0;
}

运行结果
在这里插入图片描述

3. 菱形继承

菱形继承是多继承的⼀种特殊情况。菱形继承的问题有数据冗余和⼆义性的问题。⽀持多继承就⼀定会有菱形继承,像Java就直接不⽀持多继承,规避掉了这⾥的问题,所以实践中我们也是不建议设计出菱形继承这样的模型的。

在这里插入图片描述

#include <iostream>
#include <string>
using namespace std;// 基类:人
class Person
{
public:string name;int age;
};// 老师继承人
class Teacher : public Person
{
public:string subject;void Teach(){cout << name << " 正在教授 " << subject << endl;}
};// 运动员继承人
class Athlete : public Person
{
public:string sport;void Train(){cout << name << " 正在训练项目:" << sport << endl;}
};// 学生同时继承老师与运动员
class Student : public Teacher, public Athlete
{
public:string school;void Show(){// ⚠️ 错误:编译器不知道 name 是哪个基类的// cout << "姓名: " << name << endl; // ❌ 二义性cout << "Teacher::name = " << Teacher::name << endl;cout << "Athlete::name = " << Athlete::name << endl;cout << "学校: " << school << endl;}
};int main()
{Student s;s.Teacher::name = "张老师";s.Athlete::name = "李同学"; // 各自一份 names.subject = "C++";s.sport = "800米";s.school = "宁夏大学";s.Show();// 访问需要显式指定路径s.Teacher::Teach();s.Athlete::Train();return 0;
}

Student
├── Teacher::Person
│ ├── name
│ └── age
├── Athlete::Person
│ ├── name
│ └── age
└── school
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3.1 菱形虚拟继承

如何解决菱形继承带来的弊端?——》使用虚拟继承!

以上述对象模型为例,在 Teacher 和 Athlete 继承 Person 时采用虚拟继承,即可避免 Student 同时拥有两份 Person 子对象。

  • 虚拟继承是一种专门用于解决菱形继承问题的机制,在其它普通继承场景下不建议使用。
  • 在 C++ 中,虚拟继承的使用方式是:当多个派生类继承自同一个公共基类,并且这些派生类又被同一个子类多继承时,应当在这些“中间层”父类继承公共基类的地方,使用 virtual 关键字进行修饰。(上述示例就应该在老师和运动处使用virtual进行修饰)
class Person
{
public:string name;int age;
};// 老师虚继承人
class Teacher : virtual public Person
{
public:string subject;
};// 运动员虚继承人
class Athlete : virtual public Person
{
public:string sport;
};// 学生多继承
class Student : public Teacher, public Athlete
{
public:string school;void Show(){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 构造函数调用顺序

虚继承的构造顺序也不同:

  1. 最顶层虚基类(Person)先构造。
  2. 然后是非虚中间类(TeacherAthlete)。
  3. 最后是最派生类(Student)。

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


总结底层逻辑

虚拟继承的本质可以理解为:

  1. 共享基类唯一实例 → 解决数据冗余。
  2. 虚基表存偏移量 → 动态定位虚基类成员,解决二义性。
  3. 构造顺序调整 → 确保虚基类只构造一次。
  4. 对象访问成本略高 → 需要间接查找偏移量,但换来安全和唯一性。

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

“中间类不直接存虚基类,而是存一个指向虚基类偏移的指针;派生类实例化时才分配唯一的虚基类存储。”

在这里插入图片描述


总结

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


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

http://www.dtcms.com/a/464975.html

相关文章:

  • 徐州 网站 备案 哪个公司做的好phpcms 中英文网站
  • WebSocket | 一点简单了解
  • 算法题基础 : Java : BufferedReader、BufferedWriter 和 StringTokenizer 详解
  • 企业微信 自建应用审批流程引擎功能开发【报错分析】
  • Slf4j 接口文档左侧菜单有显示,但是点击后空白
  • 【AES加密专题】4.Sbox的解析和生成
  • 考完HCIE数通,能转云计算 / 安全 / AI方向吗?
  • 重庆企业网站建设推荐怎么申请域名和备案
  • 松江 网站建设公司拼多多推广联盟
  • 中国极端气象干旱事件(1951-2022)
  • 一文详解Go 语言内存逃逸(Escape Analysis)
  • 学习threejs,实现粒子化交互文字
  • 密码学基础:RSA与AES算法的实现与对比
  • RAG:生成与检索的完美结合
  • 一款由网易出品的免费、低延迟、专业的远程控制软件,支持手机、平板、Mac 、PC、TV 与掌机等多设备远控电脑!
  • [C# starter-kit] Blazor EntityTable 组件 | 预构建
  • 深入浅出 AI Agent:从概念本质到技术基石
  • 宁波网站制作服务wordpress搭建淘客网站
  • 第五章:Go的“面向对象”编程
  • 【实用工具】mac电脑计算文件的md5、sha1、sha256
  • 数据结构算法学习:LeetCode热题100-矩阵篇(矩阵置零、螺旋矩阵、旋转图像、搜索二维矩阵 II)
  • CAD文件处理控件Aspose.CAD教程:在 Python 中将 SVG 转换为 PDF
  • Go语言游戏后端开发9:Go语言中的结构体
  • 网页网站作业制作郑州企业网站排名
  • C4D域的应用之鞋底生长动画制作详解
  • C语言自学--文件操作
  • 免费小程序网站网站建设优劣的评价标准
  • Kubernetes(K8S)全面解析:核心概念、架构与实践指南
  • 软件测试分类指南(上):从目标、执行到方法,系统拆解测试核心维度
  • 李宏毅机器学习笔记18