C++语言程序设计笔记 - 第12章 - 异常处理

第12章 异常处理

12.1 异常处理的基本思想

程序运行中的有些错误是可以预料但不可避免的,例如内存空间不足、硬盘上的文件被移动、打印机未连接好等由系统运行环境造成的错误。这是要力争做到允许用户排除环境错误,继续程序的运行;至少要给出提示信息。这就是异常处理程序的任务

在一个大型软件中,发现错误的函数往往不具备处理错误的能力,这时,它就引发一个异常,希望它的调用者能够捕获这个异常并处理这个错误。若调用者无法处理这个错误,那么还可以继续传递给上级调用者去处理,这种传递会一直持续到异常被处理为止。如果程序始终没有处理这个异常,最终它会被传递到C++运行系统那里,运行系统捕获异常后通常只是简单地终止这个程序

**C++的异常处理机制使得异常的引发和处理不必在同一函数中。**这样,底层函数可以着重解决具体问题,而不必过多地考虑对异常的处理。上级调用者可以在适当的位置设计对不同类型异常的处理。


12.2 C++异常处理的实现

C++语言提供对处理异常情况的内部支持。try,throw和catch语句就是C++语言中用于实现异常处理的机制

12.2.1 异常处理的语法

throw 表达式语法:

throw 表达式

try块语法:

try
   复合语句
catch(异常声明)
    复合语句
catch(异常声明)
    复合语句
    ...

throw语句:如果某段程序中发现了自己无法处理的异常,就可以使用throw表达式来抛掷这个异常,将它抛掷给调用者。throw的操作数表示异常类型在语法上与return语句的操作数相似。如果程序中有多种要抛掷的异常,应该用不同的操作数类型来互相区别

try语句:try子句后的复合语句是代码的保护段如果预料某段程序代码(或对某个函数的调用)有可能发生异常,就将它放在try子句之后。如果这段代码(或被调函数)运行时真的遇到异常情况,其中的throw表达式就会抛掷这个异常。

catch语句:catch子句后的复合语句是异常处理程序捕获由throw表达式抛掷的异常。异常声明部分指明了子句处理的异常的类型异常参数名称,它与函数的形参是类似的,可以是某个类型的值,也可以是引用其中的类型可以是任何有效的数据类型,包括C++的类当异常被抛掷后,catch子句便依次被检查,若某个catch子句的异常声明的类型与被抛掷的异常类型一致,则执行该段异常处理程序。如果异常类型声明是一个省略号(...),catch子句便处理所有类型的异常,这段处理程序必须是try块的最后一段处理程序

提示:异常声明的形式与函数形参的声明类似,catch子句的异常声明中也允许只指明类型,而不给出异常参数名称,只是在这种情况下在复合语句中就无法访问该异常对象了。


异常处理的执行过程:

(1)程序通过正常的顺序执行到达try语句,然后执行try块内的保护段;

(2)如果在保护段执行期间没有引起异常,那么跟在try块后的catch子句就不执行。程序从异常被抛掷的try块后跟随的最后一个catch子句之后的语句继续执行下去;

(3)程序执行到一个throw表达式时,一个异常对象会被创建。若异常的抛出点本身在一个try子句内,则该try语句后的catch子句会按顺序检查异常类型是否与声明的类型匹配;若异常抛出点本身不在任何try子句内,或抛出的异常与各个catch子句所声明的类型皆不匹配,则结束当前函数的执行,回到当前函数的调用点,把调用点作为异常的抛出点,然后重复这一过程

(4)如果始终未找到与被抛掷异常匹配的catch子句最终main函数会结束执行,则运行库函数terminate将被自动调用,而函数terminate的默认功能是终止程序

(5)如果找到了一个匹配的catch子句,则catch子句后的复合语句会被执行。复合语句执行完毕后,当前的try块(包括try子句和一系列catch子句)即执行完毕,即只要找到一个匹配的异常类型,后面的异常处理都将被忽略

细节:当以下条件之一成立时,抛出的异常与一个catch子句中声明的异常类型匹配:

(1)catch子句中声明的异常类型就是抛出异常对象的类型或其引用

(2)catch子句中声明的异常类型是抛出异常对象的类型的公共基类或其引用

(3)抛出的异常类型和catch子句中声明的异常类型皆为指针类型并且前者到后者可隐含转换


12.2.2 异常接口声明

为了加强程序的可读性,使函数的用户能够方便地直到所使用的函数会抛掷哪些异常,可以在函数的声明中列出这个函数可能抛掷的所有异常类型。例如:

void fun() throw(A,B,C,D);	//函数fun()能且只能抛掷类型A,B,C,D及其子类型的异常

**如果在函数的声明中没有包括异常接口声明,则此函数可以抛掷任何类型的异常。**例如:

void fun();	//可以抛掷任何类型的异常

一个不抛掷任何类型异常的函数可以进行如下形式的声明:

void fun() throw();	//不抛掷任何类型异常的函数的声明

细节:如果一个函数抛出了它的异常接口声明所不允许抛出的异常,那么unexpected函数会被调用,该函数的默认行为是调用terminate函数中止程序,用户也可以定义自己的unexpected函数,替换默认的函数。


12.2.3 异常处理的构造与析构

在程序中,找到一个匹配的catch异常处理后,如果catch子句的异常声明是一个值参数,则其初始化方式是复制被抛掷的异常对象;如果catch子句的异常声明是一个引用,则其初始化方式是使该引用指向异常对象

C++异常处理的真正功能,不仅在于它能够处理各种不同类型的异常,还在于它具有为异常抛掷前构造的所有局部对象自动调用析构函数的能力。

栈的解旋异常被抛出后,从进入try块(与截获异常的catch子句相对应的那个try块)起,到异常被抛掷前,这期间在栈上构造(且尚未析构)的所有对象都会被自动析构,析构的顺序与构造的顺序相反。这一过程称为栈的解旋。

一个在catch子句中声明异常参数(catch子句的参数)的例子:

catch (MyException & e) {...}

其中,也可以不声明异常参数(e)。在很多情况下只要通知处理程序有某个特定类型的异常已经产生就足够了。但是在需要访问异常对象时就要声明异常参数(e),否则将无法访问catch处理程序子句中的那个对象。例如:

catch (MyException) {
	//在这里将无法访问异常对象
}

用一个不带操作数的throw表达式可以将当前正在被处理的异常再次抛掷,这样一个表达式只能出现在一个catch子句中或在catch子句内部调用的函数中。再次抛掷的异常对象是源异常对象(不是副本)。例如:

try{
    throw MyException("some exception");
}catch(...){	//处理所有异常
    //...
    throw;		//将异常传给某个其他处理器
}

12.2.4 标准程序库异常处理

C++标准提供了一组标准异常类,这些类以基类Exception开始,标准程序库抛出的所有异常,都派生于该基类。基类Exception提供一个成员函数what(),用于返回错误信息(返回类型为const char*,在基类Exception中,what()函数的声明如下:

virtual const char* what() const throw();

标准异常类的继承关系:Exception类直接派生出bad_alloc,bad_cast,bad_typeid,bad_exception,ios_base::failure,runtime_error,logic_error类。其中,runtime_error类又直接派生出underflow_error,overflow_error,range_error类;logic_error类又直接派生出out_of_range,length_error,invalid_argument,domain_error类。runtime_error表示那些难以被预先检测到地异常,而logic_error表示那些可以在程序中被预先检测到的异常,也就是说如果小心地编写程序,logic_error类的异常能够避免

runtime_error和logic_error两个类及其派生类,都有一个接收const string &型参数的构造函数。在构造异常对象时需要将具体的错误信息传递给该函数,如果调用该对象的what()函数,就可以得到构造时提供的错误信息。

C++标准库各种异常类所代表的异常见下表:

异常类头文件异常的含义
bad_allocexceptionnew动态分配空间失败
bad_castnew执行dynamic_cast失败
bad_typeidtypeinfo对某个空指针p执行typeid
bad_exceptiontypeinfo当某个函数fun()因在执行过程中抛出了异常声明所不允许的异常而调用unexcepted()函数时,若unexcepted()函数又一次抛出了fun()的异常声明所不允许的异常,且fun()的异常声明列表中有bad_exception,则会有一个bad_exception异常在fun()的调用点被抛出
ios_base::failureios用来表示C++的输入输出流执行过程中发生的错误
underflow_errorstdexcept算术运算时向下溢出
overflow_errorstdexcept算数运算时向上溢出
range_errorstdexcept内部计算时发生作用域的错误
out_of_rangestdexcept表示一个参数值不在允许的范围之内
length_errorstdexcept尝试创建一个长度超过最大允许值的对象
invalid_argumentstdexcept表示向函数传入无效参数
domain_errorstdexcept执行一段程序所需要的先决条件不满足

一些编程语言规定只能抛掷某个类的派生类(例如Java中允许抛掷的类必须派生自Exception类),C++虽然没有这项强制的要求,但仍然可以这样实践。例如,在程序中可以使得所有抛出的异常皆派生自Exception(或者直接抛出标准程序库提供的异常类型,或者从标准程序库提供的异常类派生出新的类),这样会带来很多方便


例子:编写一个计算三角形面积的函数,函数的参数为三角形三边边长a,b,c,可以用Heron公式计算三角形面积,在计算三角形面积的函数中需要判断输入的参数a,b,c是否构成一个三角形,若三个边长不能构成三角形,则需要抛出异常。下面是源程序:

#include<iostream>
#include<cmath>
#include<stdexcept>
using namespace std;

//给出三角形三条边的长度,计算三角形的面积
double area(double a, double b, double c) throw (invalid_argument) {
	//判断三角形的三条边长是否为正
	if (a <= 0 || b <= 0 || c <= 0)
		throw invalid_argument("the side length should be positive");
	//判断三角形的三条边长是否满足三角不等式
	if (a + b <= c || b + c <= a || a + c <= b)
		throw invalid_argument("the side length should fit the triangle inequation");
	//由Heron公式计算三角形面积
	double s = (a + b + c) / 2;
	return sqrt(s * (s - a) * (s - b) * (s - c));
}

int main() {
	double a, b, c;
	cout << "Please input the side lengths of a triangle: ";
	cin >> a >> b >> c;
	try {
		double s = area(a, b, c);
		cout << "Area: " << s << endl;
	}
	catch (exception& e) {
		cout << "Error: " << e.what() << endl;
	}
	return 0;
}

程序运行结果1:

Please input the side lengths of a triangle: 3 4 5
Area: 6

程序运行结果2:

Please input the side lengths of a triangle: 0 5 5
Error: the side length should be positive

程序运行结果3:

Please input the side lengths of a triangle: 1 2 4
Error: the side length should fit the triangle inequation

C++标准程序库对于异常处理做了如下保证:C++标准程序库在面对异常时,保证不会发生资源泄露,也不会破坏容器的不变特性

(1)对于以结点实现为基础的容器,如list,set,multiset,map,multimap,如果结点构造失败,容器应当保持不变。同样需要保证删除结点操作不会失败。在顺序关联容器中插入多个元素时,为保证数据的有序排列,应当保证如果插入不成功,则容器元素不做任何改动。对于删除操作,确保删除成功。比如,对于列表容器,除了remove()remove_if()merge()sort()unique()之外的所有操作,要么成功,要么对容器不做任何改动。

(2)对于以数组实现为基础的容器,如vector,deque,在插入或删除元素时,由于有时需要调用复制构造函数和复制赋值运算符,当这些操作失败而抛出异常时,容器的不变性不能被保证。除此之外,对于这些容器的不变特性的保证程度与以结点实现为基础的容器相同。


  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值