C++继承
继承是什么、为什么会出现继承
1、什么样的场景会使用继承
xxxx我们在写代码的时候可能会出现这样的情况,就是,我们在定义各种类的时候,可能不同的类中会出现大量完全相同的重复成员,这样就造成了一定的代码冗余。举一个例子
无论是Student还是Teacher,他们作为人,姓名年龄都是他们的基本信息,如果每一个类都重复这些成员变量,这就导致了代码的冗余。当不同的类有相同的基本属性,也就意味着有相同的成员。为此,就设计出了继承来解决这个问题
2、什么是继承
xxxx继承(Inheritance)可以理解为一个类从另一个类获取成员变量和成员函数的过程。这是一种很重要的代码复用手段,继承能够使类在原有特性上进行扩展。继承是类在设计层次上的一种复用。
3、继承的语法
我先直接给出一个样例:
这就是基本的语法,还是比较容易记住的。
继承方式
xxxx继承方式限定了基类成员在派生类中的访问权限,包括 public(公有的)、private(私有的)和 protected(受保护的)。
xxxx继承方式也可以不写,使用默认的。class类默认的继承方式是private,struct类默认继承方式是public。
1、访问修饰限定符的作用
xxxx一直没有整理过访问限定修饰符作用,现在我们已经学完了它的作用,借此机会将其整理一下,让大家能够深刻理解。
(1)修饰类中成员
public(公开):能够使成员在类内类外都可以被直接访问
protected(保护):能够使成员只能在类内被访问,而不能在类外被直接访问
private(私有):能够使成员只能在类内被访问,而不能在类外被直接访问
只有public才能在类外被直接访问,但是,三种都可以在类内被直接访问
(2)修饰继承方式
继承方式\基类成员 | public成员 | protected成员 | private成员 |
---|---|---|---|
public继承 | public | protected | 不可见 |
protected继承 | protected | protected | 不可见 |
private继承 | private | private | 不可见 |
(1)我们发现,继承之后成员的访问权限 = min{父类成员访问权限,继承方式}
(2)不可见:在用子类访问时,在子类内、外都不可以访问到。但是可以通过父类访问。举例
(3)这就体现出了protected的作用,单纯考虑类中的作用,protected和private并没有什么大的区别,但是在继承这里,private会使子类对成员不可见,但是protected只会让类外不可直接访问,子类中还是可以直接访问
(4)在实际应用中,几乎不会见到把父类的成员设置为private的现象。
基类\派生类对象赋值兼容问题(切割问题)
1、基类\派生类对象赋值兼容的基本概念
我们先看一个现象
xxxx我们在这里发现,子类可以赋值给父类,但是父类不可以赋值给子类。这里就要介绍一个概念,就是**基类\派生类对象赋值兼容(切割)**下面节省,都叫切割
xxxx什么是“切割”?我们来看个图
2、其他类型的切割
xxxx刚刚我们讲到的是对象赋值给对象,我们只能将子类对象内容赋值给父类对象。
xxxx但是除了这种“对象<>对象之间的”我们还有别的方法
指针赋值
同上述类似,传递指针时,也只能将子类的地址传给父类的指针,此时该指针解引用只能访问到父类所含有的内容。
引用赋值
由于引用的底层就是指针,所以指针能够完成的,引用一般也可以。所以子类给父类引用赋值也是可以的,相当于给子类对象中继承的父类部分起了别名。此时父类的引用只能访问父类的成员。
继承中的作用域
xxxx我们都知道,在同一个作用域中,不可以定义相同的变量和函数(函数重载除外!!),但是在不同的域里确实可以存在的。那么在继承中,子类能否定义一个与父类相同的成员?
xxxx其实还是比较简单的,因为不同的类是属于不同的作用域,因此是可以定义重名成员的。例如:
xxxx虽然可以定义,但是使用的时候,我们是使用的哪一个呢?我们可以验证一下:
我们发现,A类的对象使用了A类自己的对象(这一点是很正常的)
B类的对象使用了子类的,并没有使用父类的。其实,这一点也很好解释,那就是,作用域一直是保证就近原则,在B类中有自己的_a,那么就会首先使用自己的,而不是父类的。
xxxx如果此时我们需要在子类对象中使用父类的同名的变量,我们就需要借助域作用限定符来指定作用域,从而访问到我们所需的哪一个变量。如图:
xxxx成员函数也是如此,当子类与父类具有函数名相同的函数时,还是符合就近原则。但是这里就要介绍一个概念就是隐藏\遮蔽
隐藏\遮蔽
遮蔽的概念及理解
xxxx如果派生类中的成员((包括成员变量和成员函数))和基类中的成员重名,那么就会遮蔽从基类继承过来的成员。所谓遮蔽,就是在派生类中使用该成员(包括在定义派生类时使用,也包括通过派生类对象访问该成员)时,实际上使用的是派生类新增的成员,而不是从基类继承来的。举个例子:
xxxx但是,如果我们就是想要让子类对象使用父类中的同名对象呢?**域作用限定符指定!!**如图:
遮蔽与函数重载
xxxx这个问题其实也是许多学习者困惑的地方,我也是仔细看了C语言中文网的详细解释才发现了这个问题!
xxxx函数重载是指在同一个作用域中,函数名相同,参数列表不同的函数之间形成函数重载
xxxx但是在遮蔽中,我在上面阐述概念的时候,1、没有抛开子类和父类(因此不属于同一个作用域)2、并没有提及参数的问题,只有函数名相同,并没有对参数提任何要求~
xxxx这就是继承中作用域的讲解!
派生类的默认成员函数
1、构造函数
xxxx我们先来看一个现象
xxxx我们发现,在子类中无法初始化父类的成员变量,爆出“XXX不是基或成员”、
xxxx但是我们再看一个场景
xxxx我们发现,当我们创建一个子类对象的时候,在调用构造函数的初始化列表时候,编译器会先自动调用父类的默认构造函数再去初始化子类的剩余新增内容有个大前提,就是父类必须要默认构造函数,这样编译器才会自动调用。其实这就表明,在子类中将父类的内容看做一个整体,要去初始化父类的内容,就要直接去调用父类的构造函数整体初始化。就好像我们在子类中声明了一个Person这个自定义类型的成员变量一样。(构造函数对于自定义类型会自动调用它的构造函数初始化)
xxxx但是,如果父类没有默认构造函数,就需要我们显示调用构造函数,方法如下:
2、拷贝构造
xxxx其实拷贝构造跟构造函数是几乎一样的。如果有默认的拷贝构造,那子类中的拷贝构造会直接调用默认的拷贝构造函数,如果没有,就需要我们去显示调用。
xxxx但是细心的同学会发现为啥我们去拷贝构造Person,你给它传了一个Student的对象??
xxxx记不记得刚刚提到的“切割”,父类对象是可以接受子类对象的赋值的,在这里就有这样非常好的应用!!
3、operator=( )
xxxx赋值重载与上面类似,也需要在子类赋值重载中调用父类的赋值重载。如图:
注:我们去跑这个赋值重载的代码一定会有一个bug,其实也不是很难找出,但是我认为不少人还是会小掉进这个沟里,才能再爬出来
还记不记得刚刚讲的“遮蔽”问题,子类,父类都有operator=这个函数,所以就产生了“遮蔽”,这就导致了,我们会调用子类的operator=,就会一直重复无限调用子类的operator=,发生StackOverFlow(栈溢出)。
xxxx想要解决这个问题就要指明作用域
4、析构函数
xxxx我们按照之前的观点,我们要析构,就要显示调用父类的析构函数,加入我们先这样进行操作,看看会发生什么。。。
我们发现,他报错了,原因是,在编译器处理下,所有析构函数都会被处理成一个变量名:destroy()。所以又会出现“遮蔽”的问题,所以我们就要指定作用域了!!
xxxx更改后:
xxxx但是,我们又发现了一个问题,就是为啥会先后调用两次父类的析构?那我们再试一下,如果我们不去显示调用父类析构会发生什么。。。
xxxx我们发现,这样感觉就好多了,正常了,子类和父类都只调用了一次析构函数。而且是先析构子类,再析构父类,这与我们刚刚讲解的构造函数完全一致,因为在构造函数中,编译器就会在初始化列表阶段自动调用(先显示调用)父类的构造,再去构造子类剩下的内容。由于栈的FILO特性,先构造的后析构,这里是完全吻合的。
xxxx因此,我们又得出结论对于析构函数,我们不需要显示调用父类析构函数,会在子类析构函数结束时自动调用父类的析构函数!!
多继承
单继承与多继承的概念
单继承:一个子类只有一个直接父类
多继承:一个子类有两个或两个以上的直接父类
菱形继承
xxxx菱形继承不好解释,直接看图就可以看明白
菱形继承的坑
菱形继承有两个很明显的问题:1、数据冗余。2、二义性
1、数据冗余
xxxxD类继承了B和C,但是B和C都继承了A,所以相当于D类中有两份A的数据,这两份数据都要储存在D类中,就会导致不必要的空间浪费。
2、二义性
xxxx当我们去给A类中_a赋值的时候,编译器不知道是给B类继承的A的_a赋值还是C类中的,就会产生歧义。
但是这个问题我们可以通过域作用限定符来限定作用域。例如:
虚继承
xxxx其实二义性还不是一个大问题,至少我们还有方法去解决,但是这个数据冗余的问题就是底层方面的了,我们是无法解决的。所以就出现了虚继承来解决这个问题。
xxxx在B和C类中添加virtual关键字来实现虚继承就可以解决这个问题。
终极问题:C++如何通过虚继承解决数据冗余以及二义性
xxxx到底编译器底层是怎样实现用virtual解决数据冗余的呢?(数据冗余与二义性其实是一回事,当数据冗余解决了,二义性自然也就解决了!!)
没加virtual时
我们很容易看到,B类与C类是完全独立的,所以是有两份A类(B与C的前后顺序与继承顺序有关)
加virtual时
xxxx我们把这个奇怪的数字作为地址查询,得到以下结果
对比&D的到的图,我们发现20和12是有一定意义的!!
20是就是B类相对于公共出来的A类的偏移量(4字节5个位置)
而12同样也是C类相对于公共出来A类的偏移量(4字节3个位置)
xxxx总之,当使用虚继承后,就会把冗余重复的类“提取出来”,单独放在一起,这样就只有一个A类了,就不会出现数据的冗余,也就不会有二义性了!!!
总结
继承作为面向对象语言的三大特性之一还是非常重要的。但是其实除了这个菱形继承和虚继承比较难搞之外,继承还是比较还理解的,然后重点是把语法和规律稍微记一下,还是比较容易上手的。其实一般情况我觉得也很少会出现菱形继承的情况。重难点还是接下来的“多态”,我也是一直在啃这个硬骨头,还是比较难理解的。下面我也会总结出来“多态”的知识,希望大家保持关注!