继承
概念
面向对象设计中的代码复用的最重要手段,允许在原有的基础上进行扩展,增加新功能,体现出层次结构,体现了有简单到复杂的过程
作用
- 实现代码复用
- 实现多态
继承方式
public继承
protected继承
private继承
取二者的最小值作为新的访问限定
隐藏
与函数重载的区别
- 函数重载(Overload)
作用域:必须在同一个作用域 (同一个类、同一个命名空间或全局作用域)。
条件:函数名相同,参数列表不同(参数个数、类型、顺序至少有一项不同)。
作用域:发生在继承关系中 (派生类与基类是不同作用域)。
条件:派生类定义了与基类同名的成员函数(无论参数是否相同)。
概念
如果子类和基类存在相同的成员,会优先调用子类的
成员变量的隐藏
成员函数的隐藏--函数名相同且不构成重写就是隐藏
不想隐藏->加上类域
赋值兼容
- public继承,此时是is-a的关系
- 子类可以直接赋值给基类
- 子类对象的指针和引用可以指向基类,反过来不安全
cpp
Student sobj ;
// 1.派⽣类对象可以赋值给基类的指针/引⽤
Person* pp = &sobj;
Person& rp = sobj;
// ⽣类对象可以赋值给基类的对象是通过调⽤后⾯会讲解的基类的拷⻉构造完成的
Person pobj = sobj;
//2.基类对象不能赋值给派⽣类对象,这⾥会编译报错
sobj = pobj;
默认成员函数
- 构造顺序:
基类构造
成员对象构造(Member m)
派生类自己构造 - 析构顺序:
派生类自己析构
成员对象析构
基类析构
不同继承下的对象模型
- 单继承
子类对象 = 基类部分 + 子类新增部分 - 多继承
对象布局 = 父类 1 + 父类 2 + 子类部分
问题:如果父类有同名成员 → 二义性 - 菱形继承
A
/
B C
\ /
D
问题 1:数据冗余
D 里有两份 A 的成员
问题 2:二义性
访问 A 的成员时,编译器不知道用哪一份 - 虚拟继承
菱形虚拟继承
解决数据冗余和二义性问题
虚拟继承 = 只存一份公共基类数据,中间类存偏移量,解决冗余与二义性。
考点
- 继承的优缺点
优点
体现了代码复用的思想,可以实现多态,扩展现有类的功能
缺点
增加复杂性,导致代码耦合度过高 - 什么是隐藏
在继承中,子类的成员函数或者成员变量如果与基类的名字相同,那么就会覆盖掉基类的成员,这种现象就叫做隐藏 - 基类哪些成员被继承到子类
基类的非私有成员 - 菱形继承是什么,如何解决
当有一个类同时继承了两个类,而这两个类又同时继承自一个基类,此时就会产生菱形继承
可以使用虚继承的方式进行解决 - 虚继承的原理
- 核心结构
虚基类表(vb-table)
存放虚基类相对于当前类的偏移量,通过偏移量找到唯一的那份虚基类成员。
虚基类指针(vbptr)
编译器在派生类对象头部插入的指针,指向对应的虚基类表。 - 对象布局
虚基类成员只存一份,放在对象最底部
中间类不再冗余存储,而是通过 vbptr + 偏移量 访问
从根本上解决菱形继承的数据冗余和二义性 - 构造 & 析构顺序
构造:虚基类优先构造
析构:虚基类最后析构 - 总结
虚基类本身没有 vbptr 和虚基类表
虚继承的派生类才有 vbptr 和虚基类表
所有路径共享同一份虚基类,通过偏移量访问 - 继承和组合的区别
继承和组合
public 继承 is-a
组合 has-a
组合耦合度低,继承耦合度高。优先用组合 - 如何实现不能被继承的类
私有化构造函数
使用default和delete
私有化构造函数,其他全部删除
通过友元类创建对象
使用final关键字(推荐)