5.虚函数,覆盖,多态,异常处理
一.虚函数,覆盖,多态
1.成员函数在定义时添加了virtual关键字,这种函数叫做虚函数
覆盖:如果在子类中实现了与父类中的虚函数具有相同的函数签名的函数,那么子类当中的成员函数会覆盖父类中的成员函数。
多态:如果子类中的成员函数对父类中的成员函数进行了覆盖,当一个指向子类的父类指针或者引用了子类的父类引用,当调用它的虚函数,会根据的实际调用对象调用子类中的覆盖函数,而不是父类中的虚函数,这种语法现象叫做多态。
多态的意义在于,同一种类发出同一种调用,而产生不同的反应。
二.覆盖,重载,隐藏
满足覆盖(重写)的条件:
a.必须是成员函数
b.必须是虚函数
c.函数签名必须相同
d.如果返回值是基本类型则必须相同(否则编译错误)
e.如果是类类型必须是父子类关系的指针或引用(必须能进行自动类型转换)
满足重载的条件:
a.必须在同一作用域下(父子类之间不能构成重载)
b.函数名相同但是参数列表不同
c.const属性
d.返回值的类型不会影响重载
满足隐藏的条件:
在父子类之间名字相同的标识符,只要不构成覆盖,则必定构成隐藏
三.多态的条件
1.多态特性除了父子类之间要构成覆盖,还必须是父类以指针的方式指向子类。
2.当指针或引用已经构成多态时,此时调用成员函数所传的this指针
3.在子类的构造函数执行前,先调用父类的构造函数,如果调用了被覆盖的虚函数,此时由于子类还没构造完成,因此只能是调用父类中的虚函数
构造函数在进入函数体执行时,类中看的见的资源都已经全部构造完成
4.在子类的析构函数完成后会调用父类的析构函数,如果调用被覆盖的虚函数,由于子类已经虚构完成,已经不能算作是完整的子类了,因此只能调用父类中的虚函数
四.纯虚函数和抽象类
1.纯虚函数
class A
{
public:
virtual void test(void) = 0;
virtual void test(void) const = 0;
};
a.纯虚函数不需要被实现,如果非要实现也不能在类中,必须要在类外(就变成了虚函数)
b.纯虚函数如果想要调用必须在子类中覆盖,然后以多态的方式调用。
2.抽象类
a.成员函数中有纯虚函数的叫做抽象类,这种类不能创建对象。
b.如果子类继承了抽象类,则必须把父类中的纯虚函数覆盖,否则它也变成了抽象类,不能被实例化
c.因此抽象类只能以指针或引用的方式执行子类来调用之间的虚函数
3.纯抽象类
所有的成员函数都是纯虚函数,这种类交纯抽象类
面向对象的四大特性:抽象,封装,继承,多态
纯抽象类的作用:纯抽象类是类封装的过程,同时抽象类也可以当作一个统一的接口
4.纯抽象类的应用场景:
回调模式:
函数a由程序员A实现完成,如果程序员A想调用程序员B实现的函数b.(函数指针)
函数b由程序员B实现完成,
命令模式:
输入一个命令然后执行对于的操作
生产者与消费者模式
单例模式
工厂模式:一个以专们制造其他类为己任
MVC模式
五.C++中的强制类型转换
1.C语言中的强制类型转换还能继续使用,但是不安全
2.C++的强制类型转换使用很麻烦,其实是C++之父不建议使用强制类型转换,一旦代码中需要使用强制类型转换,说明代码设计的不合理,强制类型转换是一种亡羊补牢的做法
static_cast<目标类型> 原类型静态类型转换
1.子健类型的强制转换
2.void *与其他类型指针的转换
3.父子类之间的指针转换
4.其他类型与void类型之间的转换
const_cast 去常类型转换
reinterpret_cast 重解释类型转换
整数与指针之间的相互转换
dynamic_cast动态类型转换
用于构成多态的父子类之间的指针转换
六.虚函数表
1.什么是虚函数表?当一个类中有虚函数时,编译器会为这个类分配一个虚函数表专门记录这些虚函数,在这个类的实例中会有一个隐藏的指针成员指向这张表
2.有虚函数的类,会比没有虚函数的相同的类多4个字节(具体情况还需要考虑补齐和对齐才能知道多了几个)
3.一个类只有一张虚函数表,所有的对象共享一张虚函数表
4.一般对象的前四个字节是指向虚函数表的指针变量
七.动态类型绑定
1.当使用父类的指针或引用指向子类时候,编译器并没有立即生成调用函数的指针,而是生成了一段代码,用于检查指针指向的真正对象是什么类型。
2.在代码真正运行时才通过对象的指针找到指向虚函数表的成员指针。
3.再通过成员指针访问到虚函数表,再从中找到调用的函数地址。
总而言之:使用多态会产生额外的一些代码,和调用,因此使用多态会降低代码的指向速度。
八.类的信息
1.在C++当中,使用typeid可以获取类的一些信息,以此来确定指针指向的到底是什么类
2.typeid可以直接使用来判断是否是同一种类型
typeid(类型) == typeid(类型)
3.typeid(标识符).name()获取类型名,以字符串的形式呈现
4.如果typeid(指针)只能获取到指针的类型,typeid(*指针)可以获取到对象实际的类型信息
九.虚析构
1.正常情况下,如果通过父类指针指向子类对象,当使用delete释放对象时,会只调用父类的析构函数。这个时候如果在子类中申请了资源,这样就会导致内存泄漏。
2.因此最好的解决办法是将父类的析构函数设置为虚析构函数
3.在设计类时,如果析构函数什么都不需要做,编译器也会生成一个空的析构函数,但这样的话,会让继承它的子类有安全隐患。
4.尽量把所有的析构函数都设置为虚的。
十.异常处理
1.什么是异常:能遇见但无法避免的错误
2.如何抛出异常
throw数据;
a.可以抛出基本类型的异常
throw 1;
throw "lalalal";
b.可以抛出类类型的异常
throw Student stu;
c.不要抛局部对象的指针的异常
Student stu;
throw &stu;
3.如何捕获异常
1 try{ 2 //可能会产生异常的代码 3 } 4 5 catch(异常类型1) 6 { 7 //异常处理代码1 8 } 9 catch(异常类型2) 10 { 11 //异常处理代码2 12 } 13 catch(异常类型3) 14 { 15 //异常处理代码3 16 } 17
a.在捕获异常时不光能获得异常。还能获得抛出的异常数据
b.如果异常抛出了但是没有被捕获,程序会结束
c.异常的捕获是自上而下的,不是选择是优,因此子类的异常捕获最好放在父类的前面
d.捕获异常时尽量使用引用的方式,由于抛出异常如果使用对象的方式来捕获会调用对象的拷贝构造,这样会在拷贝对象过程中再次引发异常
4.类类型的异常
a.可以为每一种异常定义一个什么都不用做的类,它知识为了区分各种异常
b.在抛出异常的时候可能会调用异常的构造,拷贝构造,赋值构造等,如果在类中有看不到的资源,一定要把这三个函数重定义
c.为了防止有自定义的异常无法被捕获,因此我们在定义异常类时,最好都继承标准库的异常类,这么哪怕,不能精准的捕获异常,也能
d.在抛异常的时候尽量抛匿名临时对象
5.编译器会生成一段用来申请"安全区"的代码并保护它,在异常发生后,此时程序的节奏已经被打乱没有哪个地方是安全的除了安全区。安全区能保证存储在此位置的异常对象不受破坏。
6.构造和析构函数中的异常
在构造函数中发生了异常后,会直接跳转到异常处理代码,异常的构造就此中断,对象的构造就不完整了,不完整的对象永远不可能调用析构函数,哪怕你用delete显示调用。
在构造函数的异常可以抛,但是不要抛出构造函数(内部处理),一般使用回滚机制
十一.文件IO
操作文件的类:ifstream,ofstream,fstream
open打开文件
打开文件的方式:
in 读 ifstream/fstream
out 写ofstream/fstream
app 以追加的方式打开,如果文件不存在则不创建,存在不清空,有写权限
ate 打开文件时定位到文件末尾
binary 以二进制方式打开文件
trunc 以清空方式打开文件
in | out
文本文件的读写使用<<,>>,与cin,cout类似
read二进制方式读文件
write二进制方式写文件
seek调整文件的位置指针