引言:
C++的异常处理机制则改变了这种面貌,使得真正的计算处理与错误处理分开。使得程序员能关注真正的计算处理工作。同时代码的可读性增加。
异常事件在C++中表示为异常对象(exception obje)。异常事件发生时,由操作系统为程序设置当前异常对象,然后执行程序的当前异常处理代码块,在包含了异常出现点的最内层的try块,依次匹配同级的catch语句。如果匹配catch语句成功,则在该catch块内处理异常;然后执行当前try...catch...块之后的代码。如果在当前的try...catch...块没有能匹配该异常对象的catch语句,则由更外一层的try...catch...块处理该异常;如果当前函数内的所有try...catch...块都不能匹配该异常,则递归回退到调用栈的上一层函数去处理该异常。如果一直回退到主函数main()都不能处理该异常,则调用系统函数terminate()终止程序。异常处理仅仅通过类型而不是通过值来匹配的,Catch块中的参数可以没有参数名称,只需要参数类型,除非使用那个参数。这也是异常处理与传统的错误处理技术的区别。
关键字:1.try 2.catch 3.throw
其中关键字try定义了一个受到监控,受到保护的代码块。关键字catch则定义了一个当try 的代码块出现异常时,进行错误处理的程序模块。并且每个catch块都带有一个参数,参数的数据类型用于与异常对象的数据类型进行匹配。throw则是检测到一个异常错误发生后向外抛出一个异常事件,由相应的catch块进行验证异常数据类型是否匹配,并决定由哪一个进行错误处理。
了解了异常处理的基本情况,我们首先写一下代码来看一下:
int main()
{
try
{
cout << "try catch Example:" << endl;
throw(1);
}
catch(int value)
{
cout << "在Catch语句中,处理异常错误,异常错误的值为" << value << endl;
}
}
程序执行结果为:
下面我们来仔细介绍下try catch 和throw
throw:
执行throw语句时,其操作数的结果作为对象被复制构造为一个新的对象,放在内存的特殊位置(既不是堆也不是栈,Windows上是放
在“线程信息块TIB”中)。这个新的对象由本级的try所对应的catch语句逐个做类型匹配;如果匹配不成功,则与本函数的外层catch
语句依次做类型匹配;如果在本函数内不能与catch语句匹配成功,则递归回退到调用栈的上一层函数内从函数调用点开始继续与
catch语句匹配。重复这一过程直到与某个catch语句匹配成功或者直到主函数main()都不能处理该异常。
因此,throw语句抛出的异常对象不同于一般的局部对象。一般的局部对象会在其作用域结束时被析构。而throw语句抛出
的异常对象驻留在所有可能被激活的catch语句都能访问到的内存空间中。throw语句抛出的异常对象在匹配成功的catch语句的结束
处被析构(即使该catch语句使用的是非“引用”的传值参数类型)。
Catch:
catch语句匹配被抛出的异常对象时,如果catch语句的参数是引用型,则该参数直接引用到throw语句抛出的异常对象上;如果catch参数是传值的,则拷贝构造一个新的对象作为catch语句的参数的值。在该catch语句结束时,先析构catch的传值的参数对
象,然后析构throw语句抛出的异常对象。
catch语句匹配异常对象时,不会做任何隐式类型转换(implicit type conversion),包括类型提升(promotion)。 异常对
象与catch语句进行匹配的规则很严格,一般除了以下几种情况外,异常对象的类型必须与catch语句的声明类型完全匹配:允许非
const到const的转换;允许派生类到基类的转换;将数组和函数类型转换为对应的指针。
我们针对catch语句中的参数来分情况写下代码,使用引用型参数和值参数。代码:
class Student
{
private:
int age;
string name;
public:
Student()
{
cout << "Student Created!" << endl;
age = 0;
name = "";
}
~Student()
{
cout << "Student Deleted!" << endl;
}
};
int main()
{
try
{
cout << "try catch Example:" << endl;
Student stu;
throw(stu);
}
catch(Student &value)//引用传递
{
cout << "在Catch语句中,处理异常错误" << endl;
}
return 0;
}
首先是使用引用型参数,执行结果:
然后我们将catch语句做了修改。修改为如下:
catch(Student value)//采用值传递
执行结果:
得到在值传递的时候有3个对象被析构了。同理是3个异常对象被创建。而在引用传递中,只有2个对象被析构,即2个对象被创建。我们注意到throw解释中有这么一句话:执行throw语句时,其操作数的结果作为对象被复制构造为一个新的对象,放在内存的特殊位置(既不是堆也不是栈,Windows上是放在“线程信息块TIB”中)。
Student stu;//这里创建一个对象,
throw(stu);//这里使用的stu 是复制过后的一个对象,存放在线程信息块TIB
//然后我们在使用值传递的时候,仍然需要队这个新创建的对象复制,如果是采用
//引用传递则直接操作这个对象。
栈展开:
栈展开(unwinding)是指当前的try...catch...块匹配成功或者匹配不成功异常对象后,从try块内异常对象抛出位置,到try块的开始处的所有已经执行了各自构造函数的局部变量,按照构造生成顺序的逆序,依次被析构。如果当前函数内匹配不成功异常对象,则从最外层的try语句到当前函数体的起始位置处的局部变量也依次被逆序析构,实现栈展开,然后再回退到调用栈的上一层函数内从函数调用点开始继续处理该异常。
catch语句如果匹配异常对象成功,在完成了对catch语句的参数的初始化(对传值参数完成了参数对象的copy构造)之后,对同层级的try块执行栈展开。
我们可以总结下异常对象的创建与析构过程:
1.首先throw异常对象的时候,其操作数的结果作为对象被复制构造为一个新的对象,放在内存的特殊位置(既不是堆也不是栈,Windows上是放在“线程信息块TIB”中)。(这里复制了一次)一般的局部对象会在其作用域结束时被析构。而throw语句抛出的异常对象驻留在所有可能被激活的catch语句都能访问到的内存空间中。
2.catch语句匹配被抛出的异常对象时,如果catch语句的参数是引用型,则该参数直接引用到throw语句抛出的异常对象上;如果catch参数是传值的,则拷贝构造一个新的对象作为catch语句的参数的值。(这里也复制了一个对象)在该catch语句结束时,先析构catch的传值的参数对象,然后析构throw语句抛出的异常对象。
3.当前的try...catch...块匹配成功或者匹配不成功异常对象后,从try块内异常对象抛出位置,到try块的开始处的所有已经执行了各自构造函数的局部变量,按照构造生成顺序的逆序,依次被析构。
写一下代码看一下:
class Student1
{
private:
int age;
string name;
public:
Student1()
{
cout << "Student1 Created!" << endl;
age = 0;
name = "";
}
Student1(const Student1& stu)
{
age = stu.age;
name = stu.name;
cout << " Student1 Copyed!" << endl;
}
~Student1()
{
cout << "Student1 Deleted!" << endl;
}
};
class Student2
{
private:
int age;
string name;
public:
Student2()
{
cout << "Student2 Created!" << endl;
age = 0;
name = "";
}
~Student2()
{
cout << "Student2 Deleted!" << endl;
}
};
int main()
{
try
{
cout << "try catch Example:" << endl;
Student1 stu;
Student2 stu2;
throw(stu);
}
catch(Student1 value)
{
cout << "在Catch语句中,处理异常错误" << endl;
}
return 0;
}
执行结果:
try
{
cout << "try catch Example:" << endl;
Student1 stu;//这里创建一个对象Student1,
Student2 stu2;//这里创建一个对象Student2
throw(stu);//这里throw抛出的异常对象的stu 是复制过后的一个对象,存放在线程信息块TIB
//然后我们在使用值传递的时候,需要抛出一个stu对象复制之后的对象,如果是采用
//引用传递则直接操作这个对象。
}
catch(Student1 value)//采用值传递(这里也会复制一个Student1对象)(匹配成功之后,就会执行栈展开,开始析构try中的stu和stu2)
{
cout << "在Catch语句中,处理异常错误" << endl;
}
try catch的嵌套分层处理
上面说到trycatch块是可以嵌套分层的,并且通过异常对象的数据类型来进行匹配,以找到正确的catch block异常错误处理代码。下面是详细叙述如何通过异常对象的数据类型来进行匹配找到正确的catch block的过程。
(1) 首先在抛出异常的trycatch块中查找catch block,按顺序先是与第一个catch block块匹配,如果抛出的异常对象的数据类型与catch block中传入的异常对象的临时变量(就是catch语句后面参数)的数据类型完全相同,或是它的子类型对象,则匹配成功,进入到catch block中执行;否则到二步;
(2) 如果有二个或更多的catch block,则继续查找匹配第二个、第三个,乃至最后一个catch block,如匹配成功,则进入到对应的catch block中执行;否则到三步;
(3) 返回到上一级的trycatch块中,按规则继续查找对应的catch block。如果找到,进入到对应的catch block中执行;否则到四步;
(4) 再到上上级的trycatch块中,如此不断递归,直到匹配到顶级的trycatch块中的最后一个catch block,如果找到,进入到对应的catch block中执行;否则程序将会执行terminate()退出。
Example:
void Func()
{
//最内层的try catch
try
{
cout << "内层try catch准备抛出一个int数据类型的异常对象" << endl;
throw 1;
}
//在内层的catch 中不能匹配,需要到调用Func函数的上层函数中匹配catch
catch(float &value)
{
cout << " 在catch block中,处理float类型异常对象" << endl;
}
}
int main()
{
try
{
Func();
cout << "外层try block,准备抛出double数据类型异常对象" << endl;
throw 0.5;
}
catch(double &value)
{
cout << "外层try catch处理double类型异常对象类型" << endl;
}
catch(int &value)
{
cout << "外层try catch处理int类型异常对象类型" << endl;
}
return 0;
}
执行结果:(可以看到被外层try catch接收到了)
void Func()
{
throw 1;
}
int main()
{
try
{
Func();
cout << "外层try block,准备抛出double数据类型异常对象" << endl;
throw 0.5;
}
catch(double &value)
{
cout << "外层try catch处理double类型异常对象类型" << endl;
}
catch(int &value)
{
cout << "外层try catch处理int类型异常对象类型" << endl;
}
Func();
cout << "zhixing " << endl;//这里执行不到,函数终止
return 0;
}
这里调用了两次Func()函数,这个函数的功能是直接抛出int类型对象,可以知道第一次调用这个函数的时候,因为在trycatch块中,所以可以catch到这个异常对象,而第二次调用的时候main函数是其最上层函数,所以没有catch可以获取这个int类型异常对象,所以会执行系统的terminate()来终止程序的运行。
catch(...)
void Func()
{
try
{
//代码真正完成的工作,有可能会抛出a,b,c类型的异常
}
catch(a &value)
{
//处理a类型异常对象
}
catch(b &value)
{
//处理b类型异常对象
}
catch(c &value)
{
//处理c类型异常对象
}
catch(...)
{
//处理a b c类型之外的所有异常对象(包括系统异常)避免遗漏
}
}