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

【C++】继承:菱形继承

希望文章能对你有所帮助,有不足的地方请在评论区留言指正,一起交流学习!

        在使用C++编写程序的时候,存在多继承的情况,这是正常的;但是多继承会存在一些问题,那就是菱形继承,虽然C++委员会已经解决了这个问题,但是还是不建议初学者使用,在了解其底层结构之后,它是很难控制的。当然继承的讲解,也有【C++】:继承全面解析-CSDN博客。

1.菱形继承

        本小节讲述什么是多继承以及单继承,。

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

        多继承:一个子类有两个或以上直接父类时称这个继承关系为多继承

菱形继承:菱形继承是多继承的一种特殊情况
class Person
{
public:string _name = "小五子";; // 姓名int age;};
class Student : public Person
{
protected:int _num; //学号string major;//专业
};
class Teacher : public Person
{
protected:int _id; // 职工编号string subject;//教授科目
};class Assistant : public Student, public Teacher
{
protected:string _majorCourse; // 主修课程
};
        菱形继承的问题:从下面的对象成员模型构造,可以看出菱形继承有数据冗余和二义性的问题。 数据冗余就是:在Assistant的对象中Person成员会有两份。

数据冗余:存在重复存储的副本不必要的额外数据
int main()
{Assistant a;return 0;
}

        创建对象,运行程序查看内存;

上图和本文中的Assistant的结构图是一样的,Student和Teacher都是有自己的Person

注意:虽然在内存中看的时候是有类在外面包着,但是真正的内存存储只有成员变量,因此才会有调用的冲突。

        二义性:同一个语法结构在编译时可以被解释成多种不同的语义,编译器无法确定程序员的真实意图。白话就是成员变量有的定义有一样的,编译器不知道调用谁。
利用上面创建的对象,我们调用一下Person 的成员变量,因为他才有重复的部分,所以才会有二义性。
int main()
{Assistant a;a._name = "小六子";return 0; 
}

上述程序可定有问题

调用不明确,二义性问题,存在重复成员变量

        要调用那个_name需要类域来区分解决二义性问题,为了方便调用,我给了Person的_name 一个缺省值 “小五子”。
利用上述创建的对象,我们访问成员变量_name。
int main()
{Assistant a;a.Student::_name = "小六子";cout << a.Student::_name << endl;cout << a.Teacher::_name << endl;a.Teacher::_name = "小七子";cout << a.Student::_name << endl;cout << a.Teacher::_name << endl;return 0;
}

        这样我们自己解决了二义性的问题。但是数据冗余问题无法解决,怎么办?

上述的程序内存较小;但是我将在Person插入一个256的字符数组呢。

        然后我们使用关键字sizeof来计算对象a 的大小

int main()
{Assistant a;cout << sizeof(a) << endl;return 0;
}

        我们将Student中的继承去掉再看;抓重放轻

计算结果

        二者相差544字节,也就是说Person单个就是544字节,对于内存小的类,可以忽略,但是就上面那种能忽略?

        菱形继承:就是在最后一层的派生类继承的时候将上层的父类重复继承了,就是重复继承

2.解决方式(虚继承的实现)

虚继承的实现

  • 编译器在虚继承的派生类对象中,会加入一个虚基表指针(vbptr)
  • 虚基表(virtual base table, vbtable) 里记录的是从当前对象指针到虚基类子对象的偏移量
  • 访问虚基类的成员时,编译器会通过 vbptr 找到虚基表,再根据偏移量找到虚基类子对象,然后访问成员。
  • 这个过程不是通过 vtable(虚函数表),而是通过 vbtable(虚基表)

        虽说二义性可以指定类域区别,但是本意是创建的结构体中Assistant,要有一个名字,并且也解决了数据冗余的问题。所以引入了虚拟继承

        还是上述的例子,我们在菱形继承的腰子处采用虚拟继承

        程序改动

        这样最终的 Assistant 里就只会保留一份基类Person的成员,既避免冗余,也消除了二义性。

        底层看一下 下述是测试程序,使用对象直接访问成员变量不会再有二义性了。

int main()
{Assistant a;a.Student::_name = "小六子";cout << a.Student::_name << endl;cout << a.Teacher::_name << endl;return 0;
}

看一下结果,

再看一下底层

执行程序

	a.Student::_name = "小六子";

        说明_Assistant中只有一个_name。使用地址再证明一下。

数据冗余是怎么解决的呢?我们继续看。我们换一个例子,来讲述,更加的明显易懂的。

   A/ \B   C\ /D
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;
};
int main()
{D d;d.B::_a = 1;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5;return 0;
}

首先我们直接使用sizeof()测试D的大小看看是否解决了数据的冗余。

未采用虚拟继承的;

为什么反而占用的内存多了呢?我们添加一个大一些的数组看看。

        可以看出虚拟继承再基类较大的时候才可以明显的区分,但是为什么数据小的时候会增加4个字节的内存呢?我们继续看。

        没有采用虚拟继承之前

        采用虚拟继承之后

可以看出,菱形继承,将重复的成员变量进行了重新安防。那么下图中的两个地址是干什么的。

调用内存看一下

0088db40指向的位置  以及0088db48

        16进制转换到十进制一个是20一个是12。我们地址之间的差距。上面的表叫做虚基表,但是对于对象d自己内存中的成员变量不是随便的调用吗,所以虚基表存在的作用是什么,往下看。

        所以20 和 12 分别是 B和C对象距离a的偏移量,所以虚拟继承的底层为了帮助对象找寻a采用了偏移量的方式。但是一个4个字节或者两个字节就可以了为什么还要使用8个字节。还有其他的值要存储 ,为其他的值进行了预留,其他的对象也是用想用的空间的,不同的对象是一样的区间?

        偏移量问题还涉及到子类赋值给父类的问题,当子类的引用或者值赋值给了父类,那么,父类将会通过偏移量来找到被重新放置的重复变量。

然后赋值之后的父类虚基表变化

看来赋值不会改变父类偏移量啊,所以偏移量的作用就是帮助对象找到a然后将其赋值给父类。

总结

机制作用表内容
虚函数表(vtable)动态绑定虚函数函数地址
虚基表(vbtable)访问虚基类成员偏移量

注意的是:

        采用虚继承的方式,派生类本身也会发生变化的,都是将基类的成员变量放在底层,给出虚基表。

总结和反思

  •  很多人说C++语法复杂,其实多继承就是一个体现。有了多继承,就存在菱形继承,有了菱 形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设 计出菱形继承。否则在复杂度及性能上都有问题。
  • 多继承可以认为是C++的缺陷之一,很多后来的OO语言都没有多继承,如Java。
 继承和组合
  • public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。
  • 组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象。
优先使用对象组合,而不是类继承 。
  •   继承允许你根据基类的实现来定义派生类的实现。这种通过生成派生类的复用通常被称 为白箱复用(white-box reuse)。术语“白箱”是相对可视性而言:在继承方式中,基类的 内部细节对子类可见 。继承一定程度破坏了基类的封装,基类的改变,对派生类有很 大的影响。派生类和基类间的依赖关系很强,耦合度高。
  •   对象组合是类继承之外的另一种复用选择。新的更复杂的功能可以通过组装或组合对象来获得。对象组合要求被组合的对象具有良好定义的接口。这种复用风格被称为黑箱用(black-box reuse),因为对象的内部细节是不可见的。对象只以“黑箱”的形式出现。组合类之间没有很强的依赖关系,耦合度低。优先使用对象组合有助于你保持每个类被装。
  •   实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有 些关系就适合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用 继承,可以用组合,就用组合。
        程序的编写需要高内聚(功能单一,),低耦合(自己的改变,对其他类的改变降低到最低)。

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

相关文章:

  • 【Rust GUI开发入门】编写一个本地音乐播放器(4. 绘制按钮组件)
  • Django小说个性化推荐系统 双算法(基于用户+物品) 评论收藏 书架管理 协同过滤推荐算法(源码+文档)✅
  • 微调数据格式详解:适配任务、模型与生态的最佳实践
  • 黑帽seo是什么做模板网站乐云seo效果好
  • 怎么编辑自己的网站企业展示型网站程序
  • java所有线程都是通过Callable和Runnable和Thread实现的
  • 0.7 秒实现精准图像编辑!VAREdit 让 AI 图像编辑告别“拖沓与失控,代码模型已开源。
  • 计算机软件包含网站开发购物网站开发设计类图
  • 【避坑实战】C# WinForm 上位机开发:解决串口粘包+LiveCharts卡顿+InfluxDB存储(免费代码+仿真工具)
  • 开源 C# 快速开发(十二)进程监控
  • 江协科技 CAN总线入门课程(仲裁)
  • Ubuntu 添加右键“复制全路径”菜单
  • 国企网站建设的意义电影影视网站模板免费下载
  • 网站主页设计模板房地产门户网站
  • 前端核心框架vue之(vuex状态篇4/5)
  • SheetGod:让Excel公式变得简单
  • 地信是“安卓”专业还是“苹果”专业?
  • 视频拼接类产品介绍
  • VSCode上配置Spring Boot环境
  • 线程同步实战指南:从 bug 根源到锁优化的终极之路
  • 中文企业展示网站模板优化wordpress后台速度
  • 做网站不赚钱了wordpress代码编辑
  • 云手机在硬件资源方面的优势
  • 技术深度解析:指纹云手机如何通过设备指纹隔离技术重塑多账号安全管理
  • 中国移动获得手机直连卫星通讯牌照:行业变革的催化剂
  • Chapter9—享元模式
  • 常州网站建设公司案例怎样做企业学校网站
  • 建设网站对企业的重要性企业网站网页设计有哪些
  • SpringBoot结合Vue 播放 m3u8 格式视频
  • 网站推广目标关键词龙岩网站设计找哪家好