目录
一、继承是什么
说到继承,其实就是将共有的数据和方法提取到一个类中,这个类就叫做父类(或基类),继承的类叫子类(或派生类)
继承体现的是类设计定义层次的复用
比如我们有学生,老师两个类,他们都有姓名,性别,年龄等属性,如果我们定义这两个类,那就会有许多类似的代码,这时候就可以用到C++的继承,可以像上面所说的,将共有的数据和方法提取到一个类中,如下所示:
这两个类,除了部分属性外,其他高度相似,所以这时候就可以用到继承这个知识点,给一个公共类person,里面有他们各自相似的属性
这时就变为了这种形式, 更加简洁明了
二、继承的使用
1、继承的格式
class A : public B
A类是子类(派生类),B类是父类(基类),public是继承方式
继承方式有:public,protected,private
2、继承的使用示例
将上面所示的教师和学生类,用public继承简单示范下如何使用
3、三种继承方式
三种继承方式和我们之前C++类和对象那里的访问限定符一样,都是有三种,公有(public),保护(protected),私有(private)
而子类的三种继承方式和父类中的三种访问方式相组合,共会有9种组合方式
4、继承方式总结
①private成员
关于父类中的private成员,子类无论用什么继承方式,均为不可见,虽然父类的私有成员也被继承到子类中了,但是C++语法限制子类不论在类内还是类外均无法访问
②父类的其他成员在子类中的访问方式
父类的其他成员在子类中的访问方式,是按照访问限定符和继承方式二者中权限较小的来进行访问的
权限大小排行:private < protected < public
例如:父类中的访问限定符是protected,而子类的继承方式是public,这时父类的其他成员在子类中的访问方式是protected,因为protected权限小于public
③需要子类访问但不允许类外被访问
当需要子类访问但不允许类外被访问时,则父类将需要子类访问的部分定为protected,这样子类继承方式如果是public或protected,这样就可以满足要求
这个需求就是关于protected限定符的由来
④继承方式也可以不显示写
使用class时,默认继承方式是private;
使用struct时,默认继承方式是public
⑤实际中多使用public
在实际中,绝大多数都使用的是public继承,因为另外两种方式的继承类外使用不了,扩展性不强
三、继承的作用域
强调一点:子类和父类是有独立作用域的
所以如果父类子类有同名函数,并且参数不同,并不是函数重载,因为并不在同一个作用域中,此时构成隐藏(成员函数的隐藏,只要函数名相同就构成隐藏)
1、隐藏的定义
当子类父类有同名成员时,子类成员会屏蔽父类对同名成员的直接访问,这种情况就叫做隐藏,也叫重定义(在子类成员函数中,可以用父类::父类成员访问)
比如,person,student两个类,person是父类,student是子类,如果子类有和父类一样的成员,如下所示:
在子类成员函数中,没有特别指定,只会访问子类的成员,如果想访问父类的成员,就需要指定作用域来访问:
如果出现隐藏,就使用上面的方式进行访问,但是在实际继承中,最好还是不要出现这种情况的
四、基类派生类的赋值转换
1、子类对象可以赋值给父类对象、指针或引用
这里父类子类既不是相同类型,也不是隐式类型转换(因为隐式类型转换不能赋值给指针)
person是父类,student是子类,语法是支持的,是进行了切片
2、切片的定义
至于为什么可以将子类对象赋值给父类对象、指针或引用,我们可以理解为子类成员是大于等于父类成员的,所以父类有的子类都是有的,那么子类给父类赋值时,可以将和父类一样的进行赋值,而父类没有的切除就行,这个行为就叫做切片
上面图可以形象说明,只有成员123赋值即可,成员45切除出去就可以
同理可知,父类对象不能赋值给子类对象
五、默认成员函数
先构造父类,在构造子类
先析构子类,后析构父类
1、默认生成的构造函数
①子类的成员和类和对象一样,内置类型不处理,自定义类型调用它的构造函数
②继承父类成员必须要调用父类的构造函数初始化
2、默认生成的拷贝构造
①子类的成员和类和对象一样,内置类型值拷贝,自定义类型调用它的拷贝构造
②继承父类成员必须要调用父类的拷贝构造函数初始化
3、默认生成的赋值
和默认构造、拷贝构造相同
4、析构函数
子类和父类的析构函数构成了隐藏
那么一个父类一个子类的析构函数构成隐藏的原因:
由于多态的需要,析构函数被统一处理成了destructtor(),所以会构成隐藏
在子类的析构函数中,不需要显示调用父类的析构函数,在每个子类的析构函数后面,会自动的调用父类的析构函数,这样可以保证先析构子类,后析构父类,不会出现人为的破坏子类父类析构函数进行的先后顺序的情况
补充一个问题:如何让一个类无法被继承?
①如果想让一个类无法被继承,可以将父类构造函数设为私有,子类是不可见的,这样如果有其他类想继承该类,在子类对象实例化时,必须要调用父类的构造函数,而父类的构造函数私有,无法访问,就可以保证让一个类无法被继承
②C++11中有语法支持,在父类后加final关键字即可完成要求
即:class A final,父类是A类,在后面加上final,子类就无法继承了
六、关于友元和静态成员
1、友元
友元关系是不能继承的,例如A是父类,B是子类,父类中有一个友元函数Print(),子类继承父类后和Print()没有任何关系
2、静态成员
若是父类定义了static静态成员,则整个继承的体系中只有一个这样的成员,无论派生多少子类,都只有一个static静态成员
七、菱形继承,菱形虚拟继承
1、单继承
一个子类只有一个直接父类
2、多继承
一个子类有两个或两个以上的直接父类
例如:子类C有两个直接父类,分别是父类A和父类B,这种继承关系就是多继承
3、菱形继承
菱形继承是多继承的一种特殊情况
A和B类同时继承D类,而C类又继承A和B类,从而构成了菱形继承
菱形继承有数据冗余和二义性的问题存在
数据冗余:C类中有A和B类的,A和B类中又有D类的,会导致C中重复出现两份一样的D类的成员的相关代码,并且在C类调用构造函数,拷贝函数等函数时,也会重复调用两次D类的相同函数,造成空间浪费等问题,从而导致数据冗余问题
二义性:C类继承了A和B类,那A和B类继承的D类的成员,在C类中想访问D类成员,不能明确知道到底是A类的还是B类的,从而形成了二义性问题。二义性问题只能在D类成员前加 A::或B:: 来明确到底是哪个类的. 像截图这种形式
要解决菱形继承的这两个问题,就需要用菱形虚拟继承来解决
4、菱形虚拟继承
菱形虚拟继承就是在中间的这两个类做以改动,加上virtual,即:
这样做可以做到访问D类中的成员时,访问的都是同一个成员
注意:在VS环境下,使用菱形虚拟继承后,内存中就不是之前那种D类A类B类的成员挨个存储了,变为了下图所示的情形,内存中变为了A类,B类,C类,最下面才是D类,并且在加virtual的两个类中,他们内存中的第一个位置存储的是一个地址(虚基表),那个地址是此时的这个存储位置距离父类D中成员位置的字节数,可以通过这个地址,来找到父类的成员
在VS环境中,内存的情况就是上面图片所显示的, 地址中存储的就是该位置距离最下方存储的公共的D类成员的字节数,比如说A类和B类都只有一个成员,并且字节数都是4,那么A类所存储的地址中存的数值就应该是20,因为父类成员的位置与该位置中间隔了20个字节的距离
地址中存储的字节数称为距离或偏移量
所以根据这个知识点,我们在计算A类和B类的大小时,还需要加上那个地址的大小
结合上面所说,一般我们不建议设计多继承,尤其是菱形继承,会在代码的复杂度和相关的性能上都有问题
八、继承和组合
1、什么叫做组合
继承上面已经说到了,组合就是下面这种样例:
即在B类中的成员类型,用A的类型,这种就叫做组合
2、组合与继承的区别与联系
继承是指每一个派生类都是一个基类对象,比如说铅笔和文具的关系,铅笔是派生类,文具是基类,铅笔是文具,这种关系我们称之为继承关系
而组合是指B类组合了A类,每个B类对象都有A类对象,比如说A类是方向盘,B类是车,车有轮胎,这种关系我们称之为组合关系
如果实际关系中,既可以继承,又可以组合,那么我们优先选择对象组合
继承:依赖关系很强,耦合度高
组合:依赖关系不强,耦合度低
所以我们多使用组合的话,相比较继承,耦合度低,代码的维护性好,不会出现一个类稍做改变,会影响许多类
这就是C++继承的相关知识了