• 【C++历险记】面向对象|菱形继承及菱形虚拟继承


    个人主页兜里有颗棉花糖💪
    欢迎 点赞👍 收藏✨ 留言✉ 加关注💓本文由 兜里有颗棉花糖 原创
    收录于专栏【C++之路】💌
    本专栏旨在记录C++的学习路线,望对大家有所帮助🙇‍
    希望我们一起努力、成长,共同进步。🍓
    在这里插入图片描述

    一、多继承以及菱形继承

    单继承:一个子类只有一个直接父类时称这个继承关系为单继承。

    比如:
    在这里插入图片描述
    多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承。

    比如:在这里插入图片描述

    菱形继承:菱形继承是多继承的一种特殊情况,指一个派生类直接或间接地从两个或者更多个基类继承成员,而这些基类又直接或间接地继承自同一个基类。

    比如:
    在这里插入图片描述

    下面是两种简单的菱形继承的模型:
    在这里插入图片描述

    二、多继承引发的问题

    C++继承体系中的多继承虽然给我们提供了代码的灵活性和重用性,但是也会引发一些问题:多继承会引发菱形继承问题,而菱形继承问题又会引发菱形虚拟继承问题。

    下面来看菱形继承的问题:
    在这里插入图片描述
    上面的对象成员模型构造中,可以看出菱形继承有数据冗余二义性的问题。在Assistant的对象中Person成员会有两份(即_age有两份)。

    • 数据冗余问题(本质就是浪费空间):存在数据重复的问题,比如Person中的_age要存储两份
    • 二义性问题访问不明确,如下图。
      在这里插入图片描述

    多继承二义性问题的解决方式

    C++是如何解决多继承带来的二义性问题呢?

    方式一:作用域解析运算符

    我们可以通过作用域解析运算符,即::来解决多继承中的二义性问题。使用作用域解析运算符来明确指定调用哪个基类的成员函数或变量。
    请看:
    在这里插入图片描述
    在这里插入图片描述

    方式二:虚拟继承

    虚拟继承:用于解决菱形继承或多继承中的二义性问题的一种机制。通过使用virtual关键字,在继承链中只创建一个共同基类的实例,从而避免了二义性。

    请看:

    class Person
    {
    public:
    	string _name; // 姓名
    	int _age;
    };
    class Student : virtual public Person
    {
    protected:
    	int _num; //学号
    };
    class Teacher : virtual public Person
    	
    {
    protected:
    	int _id; // 职工编号
    };
    class Assistant : public Student, public Teacher
    {
    protected:
    	string _majorCourse; // 主修课程
    };
    void Test1()
    {
    	Assistant as;
    	as.Student::_age = 18;
    	as.Teacher::_age = 21;
    	as._age = 24;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    解释:

    首先,有一个基类 Person,它包含了姓名 _name 和年龄 _age 两个成员变量。

    接下来有两个派生类 Student 和 Teacher,它们都以虚拟继承的方式继承自基类 Person。这样做是为了避免后续的Assistant 类在同时继承Student 和 Teacher 时,包含了两个相同的 Person 实例导致的二义性问题。

    最后,有一个派生类 Assistant,它同时继承自 Student 和 Teacher 类。由于 Student 和 Teacher都是以虚拟继承的方式继承自 Person,在Assistant 类中就只会有一个共同的 Person 实例。

    在这里插入图片描述

    调试结果如下:
    在这里插入图片描述

    在这里插入图片描述

    三、虚拟继承解决数据冗余和二义性的原理

    我们已经知道:虚拟继承是C++中的一种继承方式,用于解决多重继承中的数据冗余和二义性问题。当一个类需要从多个基类中继承,而这些基类又有共同的基类时,就会产生二义性问题。

    那虚拟继承又是如何解决这些问题的呢?

    现在我们来研究虚拟继承原理,下面是一个简化的菱形继承继承体系,请看:

    class A
    {
    public:
    	int _a;
    };
    class B : public A
    {
    public:
    	int _b;
    };
    class C : 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._b = 3;
    	d._c = 4;
    	d._d = 5;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30

    运行调试结果如下:

    监视窗口
    在这里插入图片描述

    内存窗口
    在这里插入图片描述

    可以看到上述代码中存在数据冗余的问题类D继承了类B和类C,与此同时类B和类C都继承了类A,所以可以看到在类D中有两个继承自类A的子对象,分别来自类B和类C。因此,在类D中存在数据冗余,同一个成员变量_a在类D的内存布局中会出现两次,一次来自类B的继承,一次来自类C的继承。这是因为默认情况下,多次继承同一个基类会导致该基类的成员在派生类中有多份副本

    下面来看虚拟继承是如何解决上述问题的,请看:

    class A
    {
    public:
     int _a;
    };
    class B : virtual public A//虚拟继承
    {
    public:
     int _b;
    };
    class C : virtual public A//虚拟继承
    {
    public:
     int _c;
    };
    class D : public B, public C
    {
    public:
     int _d;
    };
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    实例一:

    int main()
    {
    	D d;
    	d.B::_a = 1;
    	d.C::_a = 2;
    	d._b = 3;
    	d._c = 4;
    	d._d = 5;
    	d._a = 0;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    下面是上述代码虚拟继承的调试内存窗口:
    在这里插入图片描述

    示例二(仅仅添加了对象D d2;):

    int main()
    {
    	D d;
    	d.B::_a = 1;
    	d.C::_a = 2;
    	d._b = 3;
    	d._c = 4;
    	d._d = 5;
    	d._a = 0;
    	D d2;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    现在再来看一下内存窗口:
    在这里插入图片描述
    在这里插入图片描述

    示例三(再来看一个对象模型):

    int main()
    {
    	B b;
    	b._a = 1;
    	b._b = 2;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    在这里插入图片描述
    示例四:

    int main()
    {
    	D d;
    	d._a = 1;
    
    	B b;
    	b._a = 2;
    	b._b = 3;
    
    	B* ptr = &b;
    	ptr->_a++;
    
    	ptr = &d;
    	ptr->_a++;
    	return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    指针 ptr 指向对象 b 或对象 d 时,无论 ptr 指向的是哪个对象,当使用 ptr->_a 访问类A的成员时,编译器都会使用存储在类D对象中的偏移量来调整指针,以便正确地访问虚基类A的成员变量 _a

    好了,本文就到这里啦,再见啦友友们!!!

  • 相关阅读:
    Css基础——Css的定位
    RocketMQ 入门
    Java-NIO之Buffer(缓冲区)
    ElasticSearch7.3学习(三十二)----logstash三大插件(input、filter、output)及其综合示例
    小白学流程引擎-FLowable(三) —流程设计器Flowable UI
    【JavaSE】图书管理系统之MySQL版本
    GoLong的学习之路,进阶,标准库之并发(context)补充并发三部曲,你真的明白context吗?
    uniapp读取和写入文件
    HyperLynx(七)微带线串扰的仿真
    pytorch-激活函数与GPU加速
  • 原文地址:https://blog.csdn.net/m0_74352571/article/details/132315009