二十六、继承
1.什么是继承
1、当遇到问题时,先查看现有的类能够解决一部分问题,如果有则继承该类,
在此类的基础上进行扩展来解决问题,以此可以缩短解决问题的时间(代码复用)
2、当遇到一个大而复杂的问题时,可以先把复杂的问题拆分成若干个小问题,然后为每一个小问题设计一个类进行解决,
最后通过继承的方式把这些类汇总到一个类中,从而解决最终的大问题,以此降低问题的难度,可以同时解决问题
子类继承父类(派生类继承基类)
2.继承的语法
1、继承表
class Son: 继承表[继承方式 父类名1,继承方式 父类名2]
{
成员变量;
public: //访问控制权限
成员函数;
};
2、继承方式
public
protected
private
3. C++继承的特点:
1、C++中的继承可以有多个父类
2、子类会继承父类的所有内容
3、子类对象可以向它的父类类型转换(使用父类类型指针或者引用指向子类对象)(可以缩小),
但是父类对象不能向子类类型转换(不能使用子类类型指针或者引用指向父类对象)(不能扩大);
Base* b = new Son; true;
Base& b = son; true;
Son* son = new Base; false;
Son& son = base; false;
4、子类会隐藏父类的同名变量,在子类中直接使用同名变量时,默认使用自己的同名成员
但是可以通过 父类名::同名成员 的方式指定访问父类同名成员
5、子类与父类的同名函数是无法构成重载的,因为不再同一个作用域下,并且子类会隐藏同名的父类成员函数,
因此只能通过域限定符的方式访问父类的同名成员函数
6、在执行子类的构造函数时,会按照继承表中的顺序执行父类的构造函数,默认执行父类的无参构造,
可以在子类的构造函数的初始化列表中显示的调用父类的有参构造;最后执行子类的构造函数
Son(int num):Base(参数){} // 调用base的有参构造
7、在子类的析构函数执行结束后才会调用父类的析构函数,会按照继承表顺序,逆序执行父类的析构函数
8、当子类执行拷贝构造时,默认也会调用父类的无参构造,但是这是有问题的,因此需要在
子类的拷贝构造函数的初始化列表中显示的调用父类的拷贝构造函数
Son(Son& that):Base(that) {} // 调用Base的拷贝构造
9、当子类进行赋值操作时,默认不会调用父类的赋值操作函数,
如果需要调用父类的赋值函数时,可以通过 域限定符和赋值函数名 的方式调用
void Son::operator=(Son& that)
{
父类名::operator=(that); // 调用父类的赋值函数
}
4.继承方式和访问控制属性
访问控制属性成员的访问范围:
public 可以在任何位置访问
protected 只能在类内和子类中访问
private 只能在类内访问
继承方式的影响:
1、父类的成员是否能在子类中访问:
是在设计父类时的访问控制属性决定的,不受继承方式影响
2、但是继承方式能够决定父类成员被子类继承后,在子类中变成什么样子的访问控制属性
父类访问控制属性 public继承 protected继承 private继承
public public protected private
protected protected protected private
private private private private
3、只有使用public继承父类,父类的指针或者引用才能指向子类对象(多态的基础)
5.多重继承和钻石继承
1、多重继承:
当一个类继承了多个父类时,称为多重继承,会按照继承表的顺序在子类中排列父类的内容,
当把子类指针对象转换为父类指针时,编译器会自动计算出该父类内容所在的位置,并让指针偏移的该位置,
因此,可能会出现转换后的父类指针与转换前子类指针不相同的情况
2、钻石继承:
假如有一个类A,类B和类C都继承了类A,类D又同时继承了类B和类C,当子类的父类有共同的祖先时,称为钻石继承
1.类B和类C中都各自有类A的内容
2.类D会继承类B和类C中的所有内容,就会导致类D有两份类A的内容
3.当类D对象去访问类A的成员时,编译器会产生歧义,无法确定是那一份类A内容,无法通过编译
3、虚继承
当使用 virtual 关键字修饰继承表时,此时称为虚继承,此时子类中就会多一个虚指针,用于指向父类的内容,
当这个子类被继承时,孙子类中也会继承该虚指针,并且通过虚指针比较是否含有多份相同的祖先类,有则保留一份
因此:通过虚继承可以在钻石继承中解决子类有多份祖先类成员的问题
6.虚函数和覆盖
1、虚函数
当成员函数前加 virtual 修饰后,这样的函数称为虚函数,该类也会像虚继承一样多一个虚指针
2、虚函数表
虚指针指向一张表的首地址,该表记录的是该类中所有的想函数的首地址
((void(*)(void))(*(int*)*(int*)b))(); // 相当于调用了虚函数表中的第一个函数
3、覆盖(重写)
当使用 virtual 关键字修饰父类的成员函数,此时父类中多了一个虚指针(虚表指针)、
子类会把父类的虚指针一起继承过来,当子类中有与父类虚函数同名的成员函数时,编译器会
比较这两个函数的格式,如果格式完全相同,则会把子类的同名函数的地址覆盖掉
虚函数表中父类的同名虚函数的首地址
此时使用父类的指针或者引用指向子类对象时,调用虚函数则会调用被覆盖后的虚函数表中所
指向的子类的同名且格式相同的成员函数
4、构成覆盖的条件:
1、必须是发生在父子类,且一定为public继承
2、要呗覆盖的父类的函数必须为虚函数 virtual 修饰
3、子类中必须有与父类虚函数同名,且返回值、参数列表、常属性都必须完全相同的函数,才能构成覆盖
4、覆盖要求返回值类型相同,或者子类函数的返回值可以向父类虚函数的返回值做隐式转换且有继承关系是,可以构成覆盖
题:
重载、覆盖、隐藏、重写的区别
重载:
覆盖(重写):
隐藏:
1、如果同名但是格式不同,无论是否加 virtual ,在子类中都会隐藏父类同名成员函数
2、如果同名且格式相同,无 virtual,隐藏;有 virtual,覆盖
父子类中同名成员函数要么覆盖,要么隐藏。
7.多态
什么是多态:
是指同一个事物、指令可以有多种形态,当调用同一个指令时,它会根据参数、环境不同会做出不同的响应操作,
这种模式称为多态
根据确定执行操作的时间,多态 分为 编译时多态、运行时多态
编译时多态:
当调用重载过的函数时,编译器会根据参数的不同,在编译时就能确定执行哪个版本的重载函数,
这种叫做编译时多态,还有模板技术。
func();
func(10);
运行时多态:
在父子类中,当子类覆盖了父类的同名虚函数,然后用父类指针或者引用访问虚函数时,
它即可能调用父类的虚函数,也可能调用子类的同名函数,具体调用哪个取决于该父类指针或者指向的目标是谁,
而这个目标确定需要在运行时才能最终确定下来,这种情况叫做运行时多态
构成运行时多态的条件:
1、父子类之间且有构成覆盖关系的同名函数
2、子类是以public继承父类(让父类指针、引用指向子类对象)
3、通过父类指针或者引用访问被覆盖的虚函数
思考:
构造函数和析构函数能否为虚函数? 为什么?
8.虚构造和虚析构
虚构造:
构造函数不能设置为虚函数,假如构造函数可以设置为虚函数,子类的构造函数会自动覆盖父类的构造函数,
当创建子类对象时,子类执行自己的构造函数之前先执行父类的构造函数,但是此时父类的
构造函数已经被覆盖为子类的构造函数,就会再次调用子类的构造函数,形成死循环,
编译器不允许把构造函数设置为虚函数
虚析构:
析构函数可以设置为虚函数,当使用类多态时,通过父类指针或者引用释放对象时,
默认情况下不加虚析构是不会调用子类的析构函数,如果子类析构函数中有要释放的资源时,就会造成陷漏。
只有把父类的析构函数定义为虚析构(子类的析构函数会自动覆盖父类的析构函数),当通过父类指针或者
引用释放子类对象时,会先调用覆盖后的子类析构函数,而且之后还是会自动调用父类的析构函数,
这样不会有内存泄漏了
总结:
当使用多态且子类的析构函数中有申请内存,此时父类的析构函数一定要设置为虚函数。
9.纯虚函数和纯抽象类
纯虚函数的格式:
virtual 返回值 成员函数名(参数列表) = 0;
1、纯虚函数可以只声明不实现(一般也没必要实现)
2、父类中如果是纯虚函数,子类必须覆盖父类的纯虚函数,否则无法创建对象
3、有纯虚函数的类是无法创建对象的,因为这样的话纯虚函数就没有被覆盖
4、纯虚函数的目的就是为了强制子类去覆父类的纯虚函数,强制子类实现某些功能
5、有纯虚函数的类叫做抽象类
6、析构函数能定义为纯虚函数,需要类外实现
纯抽象类
所有的成员函数都是纯虚函数的类,叫做纯抽象类,这种类一般用于设置功能的接口,也称为接口类