[课业] 05 | C++ | 异常处理

本文详细介绍了C++中的异常处理机制,包括异常的基本信息、抛出异常的机制、多出口引发的处理碎片问题以及如何使用异常处理器和析构函数解决这些问题。通过实例解析了异常类的匹配规则和catch块的排列顺序。

异常处理基本信息

  1. 运行环境造成:内存不足、文件操作失败等
  2. 异常特征:可以预见、无法避免
  3. 常见处理方法:
    函数参数:返回值;引用参数
    逐层返回
  4. 缺陷:程序结构不清楚

抛出异常机制

整体描述

  1. 处理机制

    try
    	//监控
    	//try{<语句序列>}
    throw
    	//抛掷异常对象
    	//throw<表达式>
    catch
    	//捕获并处理
    	//catch(<类型> [<变量>])	{<语句序列>}
    	//如果异常值不重要可以直接写类型
    
  2. 细节:
    try:对语句序列进行监控,看语句中会否出现异常
    throw:抛异常,异常对象是一个表达式,值的类型可以是基本类型、类;throw会调用拷贝构造函数来copy异常对戏那个扔出去,故throw之处要能看到异常类完全的类型定义
    catch:捕获并处理;若不可处理,会接着抛到跟高层调用;调用链的块会依次退出
    throw和catch的地方不一定是同一作用域下
    切记不要抛出局部对象的指针或引用,而是直接抛出对象,后会被copy

  3. catch中异常类型的匹配规则:
    与函数重载相同,都允许一定程度的类型转换,只不过异常类型匹配的转换更加严格,仅允许三种类型的类型转换:

    非常量转化为常量
    派生类转化为基类
    数组或函数转化为指针
    除此之外,包括标准的算术类型(int, double等)都不能匹配
  4. 一个try语句后面可以接多个catch语句块,用于捕获不同类型的异常进行处理
    一例:

    void f(){
    	......
    	throw 1;
    	......
    	throw 1.0;
    	......
    	throw "abcd";
    	//C++内字符串优先解释为char*
    	......
    }
    
    try{f();}
    catch(int){...}		//处理throw 1
    catch(double){...}	//处理throw 1.0
    catch(char* ){...}	//处理throw "abcd"
    //如果f()中还有抛别的异常,后面都没处理,还是会再往上层抛
    
  5. 异常处理的嵌套

    //f->g->h 的调用关系
    f(){
    	try{g();}
    	catch(int){...}
    	catch(chat* ){...}
    }
    g(){
    	try{h();}
    	catch(int){...}
    }
    h(){
    	...
    	throw 1;		//由g捕获并处理
    	...
    	throw “abcd”;	//由f捕获并处理
    	...
    }
    

    如果所有的都没被处理,在调用链上都没被捕获,此时会直接由系统abort处理,效果为终止程序

  6. 异常类catch块的排列顺序
    一例:

    class FileErrors{};
    class NonExist:public FileError{};
    class WrongFormat:public FileError{};
    class DiskSeekError:public FileError{};
    
    int f(){
    	try{
    		WrongFormat wf;
    		throw wf;
    	}
    	catch(NonExist& ){...}
    	catch(DiskSeekError& ){...}
    	catch(FileError& ){...}
    }
    

    异常类匹配时从下往下匹配
    catch类型必须是有完全的类型声明的
    此例中:前两个类型不匹配,第三个,由于派生类转化为基类,故可以匹配

    catch块:不要将基类放在最前面

  7. 多继承下各基类都有可能catch到

  8. catch exception时,如果是对象,希望使用引用方式来catch;
    若不是,则与函数调用参数一样,会发生对象copy,没什么必要
    而且,如果希望有后续对异常对象的处理,catch引用还便于直接改变对象

一例:

class MyExceptionBase{};
class MyExceptionDerived:public MyExceptionBase{};
void f(MyExceptionBase& e){throw e;}
int main(){
	MyExceptionDerived e;
	try{f(e);}
	catch(MyExceptionDerived& e){
		cout << "MyExceptionDerived" << endl;
	}
	catch(MyExceptionBase& e){
		cout << "MyExceptionBase" << endl;
	}
}

该程序可以正确编译,输出内容为

MyExceptionBase
因为throw e时用的是拷贝构造函数,按照静态类型拷贝;故原本的派生类对象被切片,main函数中catch到的是基类对象
  1. 特例
    无参数throw:将捕获到的异常对象重新抛掷出去

    catch(int){throw;}
    

    默认异常处理:表示捕捉所有异常

    catch(...)
    
  2. 系统自动调用函数的异常处理
    构造函数:抛出异常,也希望构造函数部分处理;

    1. 特别之处在于构造函数的初始化列表也会抛异常;解决方法为:(由于try{}可包含一个语句块/初始化列表)直接将构造函数的参数列表后(即初始化参数列表之前)就写上一个try语句,那么这个try的catch也可以处理初始化列表抛出的异常
    2. 构造函数的参数异常属于调用给他参数的地方发生的异常,不用构造函数自己处理

    析构函数:必须在自己函数体内进行处理

  3. 对程序验证特征进行支持

    template<class T, class E>
    inline void Assert(T exp, E e){
    	if (DEBUG)
    		if(!exp)
    			throw e;
    }
    

多出口引发的处理碎片问题

同一处本可以处理多个不同地方产生的异常,这解决了重复代码问题,但是异常本虽然解决了catch的重复化代码问题,抛出异常的各个点给出了另一种(除return之外)对出口,throw语句变成了控制流的出口,此时就需要处理多出口引发的处理碎片

注:Java中的finally语句就是用来处理多出口引发的处理碎片的

C++中当有多个点需要共性任务处理时,有两种不影响对象布局的方式去处理:

  1. 异常处理器:可以处理多处发生的异常

  2. 析构函数
    析构函数常用于将资源初始化成对象,由析构函数负责资源的处理、清空;因此,对异常引发的多出口问题,一样可用析构处理
    一些语句必须在函数结束时执行,将这些语句作为析构函数体,相应对象与需要执行的函数放在同一生命周期内;这样,在函数出口时必然会析构这个对象,就执行了善后工作
    RAII思想取代了finally功能

总结

哪些函数可以有C++编译器自己提供:

class Empty{};
//等价于
class Empty{
	Empty();							//构造函数
	Empty(const Empty& );				//拷贝构造函数
	~Empty();							//析构函数:防止资源泄漏
	Empty& operator=(const Empty& );	//赋值操作符的默认重载
	Empty* operator&();					//取地址符重载
	const Empty* operator&() const;		//取地址符重载
};

例子:




   
___Fin___
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值