C++_继承(菱形继承与虚基表)

Posted 楠c

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C++_继承(菱形继承与虚基表)相关的知识,希望对你有一定的参考价值。

1. 什么是继承

继承是什么:
继承是面向对象程序设计、使得代码可以复用的重要手段,它允许程序员在保持原有特性的基础上进行扩展,增加功能,这样产生的新类,称之为派生类。

继承的目的:

让子类继承和复用父类定义的成员和方法、继承下来的变量是独立的、各自拥有各自的内存空间。
在这里插入图片描述

2. 继承方式和访问限定符

在这里插入图片描述

  1. 可见在派生类中,子类的访问方式是,MIN(继承方式,基类访问方式)

  2. 实际基类的private成员在派生类不可见,只是语法上无法访问,假如你调用基类的方法还是可以用的。
    在这里插入图片描述
    但是调用继承的方法可以用
    在这里插入图片描述

  3. 使用关键字class时默认的继承方式为private,使用struct时默认的继承方式是public,建议显示的写出继承方式

  4. 基类private成员在派生类中是不能被访问,如果基类成员不想在类外直接被访问,但需要在派生类中能访问,就定义为protected。可以看出保护成员限定符是因继承才出现的。

  5. 实际运用中,一般都是使用public继承,很少并且不提倡用protected和private继承,因为这两种继承下来的成员都只能在子类的类里面使用,实际中扩展延伸性不强

2.1 基类与派生类赋值

  1. 派生类对象可以赋值给基类的的对象/指针/引用,这种方式叫做切片或者切割。即将子类中父类拿部分切下来,赋值过去
    在这里插入图片描述
    而且当发生切片的时候,_name是string类型,还会深拷贝。
    在这里插入图片描述

应用场景:
在这里插入图片描述
在这里插入图片描述

形参并没有发生隐式类型转换,假如是隐式类型转换的话,形参是肯定要加const的,因为引用的是临时对象

  1. 基类对象不能赋值给派生类对象
    类似于:参数是单向迭代器时,双向的可以传入,是双向迭代器的时候,单向的不可传入
  2. 基类的指针可以通过强转可以赋值给派生类的指针,前提是这个基类的指针必须指向一个派生类对象

在这里插入图片描述

  1. 所有的前提都是在public继承下才会存在

2.2 继承中的作用域

1.在继承体系中基类和派生类都有独立的作用域(即可以定义同名变量)

2.子类和父类中有同名成员,子类成员将屏蔽父类对同名成员的 直接访问,这种叫做隐藏,也叫做重定义
类似于全局变量和局部变量同名,就近使用局部变量。
在子类成员函数中,可以使用基类::基类成员 显示访问

3.需要注意的是如果是成员函数的隐藏,只需要函数名相同就构成隐藏

4.注意在实际中在继承体系里面最好不要定义同名的成员

2.3 派生类的成员函数

2.3.1 构造函数

这种方法是错误的。
在这里插入图片描述
这里将它看做一个对象。调用他的构造函数,不写的话会自己调用默认构造函数(Tom)
在这里插入图片描述

假如没有默认构造函数的话必须在参数列表写(jerry)

2.3.2 拷贝构造

在这里插入图片描述

2.3.3 赋值重载

在这里插入图片描述
上述四个,都是先调用父类的成员函数,然后再自己处理自己的成员变量

2.3.4 析构函数

析构函数十分特别。
在这里插入图片描述
第一个问题,竟然报错了,其实这里是构成了隐藏,但是函数名明明不一样为什么就构成隐藏了呢?

由于多态的需要,所有的析构函数都被处理成了 destructor,所以他们的名字在编译器看来都是一样的。所以需要显示调用。
第二个问题,这竟然调用了两次person的析构。
在这里插入图片描述
梳理一下过程,定义子类对象,由于参数列表中,初始化的顺序是声明的顺序,所以无论怎么写,都是父类构造函数先构造,然后在构造子类的。但是先析构的是子类对象。假如我们显示的调用就会使父类先析构。
所以在汇编层面他会自己在子类成员析构后,自动调用父类成员。

3. 继承与友元

友元关系不能继承,也就是说基类的友元不能访问子类私有和保护成员。要使用的话必须重新声明

4. 继承与静态

基类定义了static静态成员,则整个继承体系里面只有一个这样的成员。无论派生出多少个子类,都只有一个static成员实例 。

5. 菱形继承

菱形继承是C++继承设计的一个缺陷,由于需要不断向前兼容,结果越陷越深,解决问题又要引入新的问题。
这是一个单继承,不存在问题
在这里插入图片描述
在这里插入图片描述
多继承用“,”分割,但是它引发出一个问题。

在这里插入图片描述

那就是菱形继承,一定会引起两个问题,数据冗余和二义性

5.1 菱形虚拟继承

在这里插入图片描述
Person中有_name,Student,Teacher也一定会继承下来。由此助手类继承了Student和Tcacher,这样继承了两个_name。所以
在这里插入图片描述
所以我们使用的时候就必须加上访问限定符
在这里插入图片描述
所以二义性就可以这样解决。但是数据冗余怎么解决呢?
使用virtual,虚继承来解决。
在这里插入图片描述

在这里插入图片描述
注意,是在Student和Teacher类,继承时加virtual。
Assisant不变。
在这里插入图片描述

在这里插入图片描述
所以显示调用解决了二义性问题,但没有解决数据冗余,而virtual既解决了数据冗余而且也解决了二义性。

5.2 虚基表

在这里插入图片描述

D d;
sizeof(d);

显然是20
是因为d中继承了B和C里的成员。显然没有虚继承之前有着数据冗余和二义性。
在这里插入图片描述

将B,C两个类加上虚拟继承,那么加入虚拟继承后的类是多大呢?
在这里插入图片描述

前面提到,虚拟继承解决了数据冗余和二义性,只有4个成员变量。
此时是否为16呢?
在这里插入图片描述
研究一下底层。
在这里插入图片描述
原本不加虚继承之前,继承了B中的a和b,继承了C中的a和c。对d对象取地址,对应的地址存放着有效值。可以看到加了虚继承后,原本那块地址对应的是一个冗余有效数据,现在变成了地址,也就是说那块地址现在存了一个指针。我们把它叫做虚基指针。它又指向一个虚基表。原本的两个a现在变成一个a,这个a,现在是一个虚基类对象,存储在最下面。
在这里插入图片描述
那么这个虚基指针指向的虚基表有什么用呢?

在这里插入图片描述
虚基表存着当前位置距离虚基类对象的偏移量,而存储着虚基指针的那个地址地址通过虚基指针拿到偏移量,相加得到之后,就指向了最下面的公共虚基类对象。这样就完成了处理数据冗余和二义性的问题。
来看一个例子:

B& rb=d;
rb.a=1;

d对象继承了B对象的a,rb先找到继承B对象的a的地址(引用的是C类型的话,就找c对象对应的a),不过此时由于虚继承那块地址不在存储有效值,而是存储这一个虚基指针,指向一个虚基表,存储着偏移量,然后在指向公共的虚基类对象,从而修改。

有了多继承就会有菱形继承,有了菱形继承就会引进数据冗余和二义性,而C++用虚拟继承解决了这一问题,虚拟继承底层实现了,让那块地址对应的区域不在存储冗余数据的值,而是存储一个虚基指针,指向一个虚基表,虚基表存储着偏移量,对应类型来找自己类型的a时(B类型找b的a,C类型找c的a),通过+偏移量,找到公共的虚基a对象。

6. 继承的总结与反思

  1. 有了多继承,就存在菱形继承,有了菱形继承就有菱形虚拟继承,底层实现就很复杂。所以一般不建议设计出多继承,一定不要设计出菱形继承。否则在复杂度及性能上都有问题。

  2. 多继承可以认为是C++的缺陷之一,很多后来的OO语言都没有多继承,如Java。

  3. public继承是一种is-a的关系。也就是说每个派生类对象都是一个基类对象。组合是一种has-a的关系。假设B组合了A,每个B对象中都有一个A对象
    在这里插入图片描述
    例如动物和狗,更符合is-a,那么就用继承。头和眼睛,更符合has-a,用组合。假如两个都比较符合就用组合

  4. 实际尽量多去用组合。组合的耦合度低,代码维护性好。不过继承也有用武之地的,有些关系就适
    合继承那就用继承,另外要实现多态,也必须要继承。类之间的关系可以用继承,但能用组合,就尽量用组合。符合高内聚低耦合。

以上是关于C++_继承(菱形继承与虚基表)的主要内容,如果未能解决你的问题,请参考以下文章

钻石(菱形)继承和虚基类

C++中的各种“虚“-- 虚函数纯虚函数虚继承虚基类虚析构纯虚析构抽象类讲解

C++中的各种“虚“-- 虚函数纯虚函数虚继承虚基类虚析构纯虚析构抽象类讲解

虚继承与虚函数继承

C++ Primer 5th笔记(chap 18 大型程序工具)构造函数与虚继承

虚基类菱形派生关系