知识铺垫
首先我们要了解一下,什么是异常Exception:
异常Exception:
An exception is an indication of a problemthat occurs during a
program’s execution.
程序执行期间,可检测到的不正常情况
例如:除数为零、数组下标越界、打开不存在的文件、内存分配失败等
常见的标准库异常类:
- invalid_argument异常:接收到一个无效的实参,抛出该异常
- out_of_range异常:收到一个不在预期范围中的实参,则抛出
- length_error异常:报告企图产生“长度值超出最大允许值”的对象
- bad_alloc异常:当new()操作符不能分配所要求的 存储区时,会抛出该异常。它是由基类exception派生的
- overflow_error异常:报告算术溢出错误
- underflow_error异常:报告算术下溢错误
以上异常被包含在stdexcept库中
基础处理方法
在很多情况下,当异常出现后,我们需要合理的处理异常,使程序能够继续运行
常见的处理异常的方法就是:
try -> catch -> throw
基本结构就是:
//捕获并处理异常的程序段
try
throw 异常对象;
catch(异常类型声明)
复合语句
catch(异常类型声明)
复合语句
...
- throw操作创建一个异常对象并抛掷
- catch子句按其出现的顺序被检查,匹配的catch子句将捕获并处理异常(或继续抛掷异常)
- 特定异常类型变量的声明,如
catch(bad_alloc& theexception)
- 如要捕捉所有的异常,则使用
catch( … )
throw 对象名
- throw表达式:在try块中,用于抛出异常,是异常处理的第一步
- throw表达式也可以抛出任何类型的对象,如枚举、整数等等,但最常用的是类对象。
catch(异常类型声明){ 复合语句 }
- 包括三个部分:
- 关键字catch
- 圆括号中的异常声明
- 复合语句
- 异常类型声明:可以是基本数据类型,也可以是用户自定义类型。只能声明一种类型
catch (…) { 复合语句 }
- catch_all子句
- 任何异常都可以进入这个catch子句
- 这里的三个点称为省略号
- 花括号中的复合语句用来执行指定操作
- 异常发生后按栈展开(stack unwinding)退出,动态分配的非类对象资源不会自动释放的,通常在catch_all子句中释放
- catch_all子句可以单独使用,也可以与其它catch子句联合使用
如果联合使用,则必须放在相关catch子句表的最后 - 如果catch_all子句放在前面进行某项操作,操作完成之后该异常还需要进入特定的catch语句中进行其他操作,则应由catch子句重新抛出异常,逆调用链去查找匹配的处理子句来处理
程序执行步骤:
- 抛出异常时,跳过try块中的后续语句,try块结束,程序控制转至catch语句
- 寻找匹配的catch handler,遵循 " is a " 准则,即如果抛出的异常一个对象,则派生类对象可以与基类的catch handler成功匹配
- 执行catch handler代码
- 执行后,跳过剩余的catch语句,程序控制跳至try-catch块后的首条语句
- 若无匹配的catch handler,则交给C++标准库中定义的terminate()处理异常, 其缺省功能是调用abort()终止程序
#include <iostream>
using namespace std;
void fun(int x)
{
try {
if (x==0) throw x+5;
if (x==1) throw 'X';
if (x==2) throw "a book";
if (x==3) throw 3.14;
if (x==4) throw true;
}
catch(int i) {cout<<"catch an integer "<<i<<endl;}
catch(char c) {cout<<"catch a char "<<c<<endl;}
catch(char str[10]) {cout<<"catch a string "<<str<<endl;}
catch(double d) {cout<<"catch a double "<<d<<endl;}
catch (...) {cerr << "caught other exception (non-compliant compiler?)\n";}
}
int main()
{
fun(0);
fun(1);
fun(2);
fun(3);
fun(4);
return 0;
}
// Output
catch an integer 5
catch a char X
catch a string a book
catch a double 3.14
catch other exception (non-compliant compiler?
在这里强调一下异常处理的适用情况:
- 异常处理只用于异常情况
- 程序本身的局部错误可用传统控制方法
- 处理库函数产生的错误时通常用异常
处理除数为零的异常
#include<iostream>
using namespace std;
int divd(int x,int y) {
if (y==0) throw y; //抛出异常
return x/y;
}
int main()
{
try {
cout<<"5/2="<<divd(5,2)<<endl;
cout<<"8/0="<<divd(8,0)<<endl;
cout<<"7/1="<<divd(7,1)<<endl;
}
catch (int) {cout<<"except of deviding zero.\n";}
cout<<"that is ok.\n";
}
之前我们强调过:异常类型声明可以是基本数据类型,也可以是用户自定义类型。只能声明一种类型
所以我们利用runtime_error异常类派生出一个新的类,重写上面的代码
#include<stdexcept>
using std::runtime_error
class DivideByZeroException:public runtime_error {
//继承runtime_error类
public:
DivideByZeroException():runtime_error("attempt to divided by zero") {}
};
这里我们可以注意到,C++标准类runtime_error在构造的时候,可以提供一个字符串类型的参数,初始化异常的类型
当在程序中调用what成员函数时,就可以输出这段字符串了
#include"DivideByZeroException.h"
double quotient(int numerator,int denominator)
{
if (denominator==0)
throw DivideByZreoException();
return static_cast<double>(numerator)/denominator;
}
int main()
{
int number1,number2;
double result;
cout<<"Enter two integers (end-of-file to end):\n";
while (cin>>number1>>number2)
{
try
{
result=quotient(number1,number2);
cout<<"The quotient is: "<<result<<endl;
}
catch (DivideByZeroException ÷ByZero)
{
cour<<"Exception occurred: "<<divideByZero.what()<<endl;
}
cour<<"\nEnter two integers (end-of-file to end):\n";
}
return 0;
}
异常再抛出
我们在介绍catch_all子句的时候,说过这样一段话:
如果catch_all子句放在前面进行某项操作,操作完成之后该异常还需要进入特定的catch语句中进行其他操作,则应由catch子句重新抛出异常,逆调用链去查找匹配的处理子句来处理
小朋友你是否有一个疑问:怎么重新抛出异常?
其实这个问题很简单,只需要短短一个词的指令:
throw; //异常再抛出
- 由外层的catch捕捉
- 外层无catch处理器时,调用terminate终止程序
栈展开
throw可以抛出临时对象(异常对象)
try
{
if (IsEmpty()) throw popOnFull<T>(data);
catch (popOnFull<T> eObj) {
cerr<<"栈满"<<eObj.getvalue()<<"未压入栈"<<endl;
}
}
这个异常对象在抛出点被创建,catch子句处理完之后生命周期结束
catch子句的异常声明与函数参数声明类似,可以是按值传送,也可以按引用传递
对于大型类,建议按引用传递
catch (pushOnFull<T> &eObj) {
cerr<<"栈满"<<eObj.getvalue()<<"未压栈"<<endl;
}
下面我们就要来介绍异常处理中最重要的一个机制了:Stack Unwinding(栈展开机制)
- 当某个函数(异常源)抛出异常,将立刻结束该函数的执行,根据函数调用链回溯(可以是本函数)寻找可以catch该异常的Handler
- 如果找到了匹配的Handler,则执行Stack Unwinding,一次释放从异常源到Handler所在函数的所有局部对象
- 如果在main函数中仍没有找到匹配的Handler,则调用terminate函数(该函数缺省调用abort)
#include<iostream>
#include<stdexcept>
using namespace std;
void function3() {
cout<<"In function3"<<endl;
throw runtime_error("runtime_error in function3");
}
void function2() {
cout<<"function3 is called inside function2"<<endl;
function3();
}
void function1() {
cout<<"function2 is called inside function1"<<endl;
function2();
}
int main()
{
try {
cout<<"function1 is called inside main"<<endl;
function1();
}
catch (runtime_error &error) {
cout<<"Exception occurred: "<<error.what()<<endl;
cout<<"Exception handled in main"<<endl;
}
}
// Output
function1 is called inside main
function2 is called inside function1
function3 is called inside function2
In function3
Exception occurred: runtime_error in function3
Exception handled in main
构造函数,析构函数和异常处理
异常对象被catch语句匹配时,为已构造的所有局部对象自动调用析构函数
如果catch语句的异常类型声明的是一个值参数, 则其初始化方式是复制被抛出的异常对象
如果是引用,则初始化方式是使用引用指向异常对象
#include <iostream>
#include <string>
using namespace std;
class Except {
public:
Except() {}
Except(const Except &) {cout<<"Copy Constructor is called\n";}
~Except(){}
const char *showReason() const {return "Except类异常";}
};
class A {
public:
A() {cout<<"构造A."<<endl;}
~A() {cout<<"析构A."<<endl;}
};
void Func() {
A a;
cout<<"在Func中抛出Except类异常"<<endl;
throw Except();
cout<<"会显示吗?"<<endl;
}
int main()
{
cout<<"在main函数中"<<endl;
try {
cout<<"在try块中调用Func()"<<endl;
Func();
}
catch(Except e) {cout<<"捕获到Except类型异常:"; cout<<e.showReason()<<endl;}
catch(...) {cout<<"捕获到其它异常"<<endl;}
cout<<"回到main()函数,恢复执行"<<endl;
return 0;
}
继承和异常处理
如果catch捕获基类类型异常对象的指针或引用, 则可以捕获该基类所派生的异常对象的指针或引用,即允许多态处理错误
exception类的接口如下:
namespace std { //注意在名字空间域std中
class exception {
public:
exception(); //缺省构造函数,指定该函数不抛出任何异常
exception(constexception &); //拷贝构造函数
exception &operator=(const exception&); //赋值操作符重载
virtual ~exception(); //析构函数
virtual constchar* what()const; //返回一个C风格的字符串,为抛出的异常提供文本描述
};
}
处理new的异常
如果我们使用newf申请内存时失败,且set_new_handler并没有声明new handler函数,new指令会抛出一个bad_alloc类型的异常
我们有三种方法,来处理这个异常:
- 声明new handler函数
- 使用unique_ptr类
- 直接处理bad_alloc异常
Exception handle典型操作:
- 向用户报告错误
- 将错误记录到文件中
- 优美地终止程序
- 尝试用另一种策略来完成失败的任务
声明new handler函数
- 函数set_new_handler在出现内存申请失败的时候,会被new指令调用
- 函数指针作为参数,该函数没有参数和返回值
- C++中标准的new-handler函数
- 创造更多的内存,并且再次尝试new申请内存
- 抛出一个bad_alloc类型的异常,或者
- 调用函数abort或exit终止程序
#include <iostream>
using std::cerr;
using std::cout;
#include <new> // standard operator new and set_new_handler
using std::set_new_handler;
#include <cstdlib> // abort function prototype
using std::abort;
// handle memory allocation failure
void customNewHandler() {
cerr<<"customNewHandler was called";
abort();
}
// using set_new_handler to handle failed memory allocation
int main()
{
double *ptr[50];
// specify that customNewHandler should be called on
// memory allocation failure
set_new_handler(customNewHandler);
// allocate memory for ptr[i]
// customNewHandler will be called on failed memory allocation
for (int i=0;i<50;i++) {
ptr[i]=new double[50000000]; // may throw exception
cout<<"Allocated 50000000 doubles in ptr["<<i<<"]\n";
}
return 0;
}
使用unique_ptr类
C++智能指针是包含重载运算符的类
其行为像常规指针,但智能指针能够及时、妥善地销毁动态分配的数据
智能指针类重载了解除引用运算符 *
和成员选择运算符 ->
unique_ptr是从C++ 11开始,定义在< memory >中的智能指针,持有对对象的独有权
当超出作用域时,智能指针所指向的对象会被自动释放
class Integer {
public:
Integer(int i=0);
~Integer();
void setInteger(int i);
int getInteger() const;
private:
int value;
};
Integer::Integer(int i):value(i) {cout<<"Constructor for Integer "<<value<<endl;}
Integer::~Integer() {cout<<"Destructor for Integer "<<value<<endl;}
void Integer::setInteger(int i) {value=i;}
int Integer::getInteger() const {return value;}
int main() {
cout<<"Creating a unique_ptr object that points to an Integer\n";
unique_ptr<Integer> ptrToInteger(new Integer(7));
cout<<"\nUsing the unique_ptr to manipulate the Integer\n";
ptrToInteger->setInteger(99);
cout<<"Integer after setInteger: "<<(*ptrToInteger).getInteger()<<"\n\nTerminating program"<<endl;
}
直接处理bad_alloc异常
int main()
{
double *ptr[50];
try {
for (int i=0;i<50;i++) {
ptr[i]=new double[50000000]; // may throw exception
cout<<"Allocated 50000000 doubles in ptr["<<i<<"]\n";
}
}
catch (bad_alloc &memoryAllocationException)// handle exception
{
cerr<<"Exception occurred: "<<memoryAllocationException.what()<<endl;
}
cout<<"Exception handled."<<endl;
return 0;
}
应用实例
为类模板Array重新定义下标操作符[],如果索引值越界,它会抛出一个out_of_range类型的异常
template<typename elemType>
class Array {
public:
Array(int sz=DefaultArraySize) {
size=sz;
ia=new elemType[size];
}
~Array() {delete []ia;}
elemType & operator[](int ix) const { //下标运算符[ ]重载
if(ix<0||ix>=size) { //增加异常抛出,防止索引值越界
string eObj="out_of_range error in Array< elemType >::operator[]()";
throw out_of_range(eObj);
}
return ia[ix];
}
private:
int size;
elemType * ia;
};
int main()
{
int i;
Array<int> arr;
try {
for (i=0;i<=DefaultArraySize;i++) {
arr[i]=i+1; //写入ia[10]时出界
cout<<setw(5)<<arr[i];
}
cout<<endl;
}
catch(const out_of_range & excp) {
cerr<<'\n'<<excp.what()<<'\n'; //打印"out_of_range error in array<elemType>::operator[]()"
return -1;
}
return 0;
}