C++异常和断言
目录
一,异常的概念
二,异常的分类
三,异常处理的基本思想
四,异常处理
1,异常处理机制
2,异常处理结构
3,异常处理的基本流程
(1)检测异常try
(2)抛出异常throw
(3)捕获异常catch
(4)案例:除零异常的检测,抛出和捕获
(5)异常处理规则的强调
4,异常处理模式
5,异常规范说明
五,异常类
六,捕获异常
1,重抛异常
(1)采用普通方式重抛异常
(2)采用标准语法重抛异常
2,捕捉所有异常
七,标准库中的异常处理
1,语言本身支持的异常:此类异常用于支持某些语言特性
2,C++标准程序库发出的异常:派生自logic_error(逻辑异常)
3,程序作用域之外发生的异常:派生自runtime_error(运行时异常)
4,特别的是
八,断言
1、断言概述
2、断言的使用
一,异常的概念
异常——是指在程序运行过程中,由于系统条件、操作不当等原因而引起的运行错误。
常见的异常包括:除零错误、指针访问受保护空间、数据越界、分配空间失败、要打开的文件不存在等。
二,异常的分类
(1)可预见的异常
通过在程序结构中添加相应的容错代码进行处理。
(2)不可预见的异常
利用C++提供的一些内置的语言特性来处理
三,异常处理的基本思想
通常程序由多个模块组成,程序中出现异常和处理异常可能分布在不同模块中。C++异常处理的基本思想是:让一个函数在发现了自己无法处理的错误时抛出一个异常,然后它的直接或间接调用者能够处理这个问题,实现了问题检测和处理的分离。
四,异常处理
1,异常处理机制
C++中的异常处理机制有三部分组成:
抛出异常:若在某一个模块中检测出发生异常,当前模块中无法处理该异常,将通过抛出异常的方式将包含有异常信息的对象发送到模块外部。
捕捉异常:若某模块能够处理该异常,则该模块将获得程序控制权来处理该异常。
处理异常:捕捉到异常后,按照一定策略对异常进行处理。
C++中引入try、catch、throw关键字用于实现异常处理,通常将一组可能产生异常的代码放在try包含的块内,若发生异常,则在块内通过throw抛出异常,紧随try之后的某个catch结构用来接收并处理该异常。
通过抛出异常和异常处理的分离,使程序各部分只需完成自己的工作,保证了程序的高容错性,而抛出异常和处理异常之间还可以互相通信,保证了程序各部分的有机结合。
标准库中的异常类:标准库中提供了一组异常类在抛出异常和处理异常间传递信息。
2,异常处理结构
C++中通过try、throw、catch结构实现了异常处理,try、throw用于异常检测及抛 出异常,catch结构用于处理异常。
抛掷异常的程序段:
void Fun()
{
……
throw 表达式;
……
}
捕获并处理异常的程序段:
try{ //异常检测
……
throw 异常类型1对应的异常值; //抛出异常:异常值或异常对象
}
catch (异常类型1) //捕获异常
{
进行异常处理的语句
}
catch (异常类型2)
{
进行异常处理的语句
}
try块检测异常,若出现异常则由throw抛出。抛出异常后从各catch子句中寻找与抛出异常类型匹配的结构,匹配成功时进入该catch子句完成异常处理。
3,异常处理的基本流程
如果在try结构中没有引起异常,catch结构不会执行。程序从try结构后跟随的最后一个catch子句后的语句继续向下执行。
catch子句会依次检查异常类型是否与声明的类型匹配;若异常抛出点不在try结构 中或与各个catch子句声明的类型皆不匹配,则结束当前函数,回到当前函数的调 用点,把调用点作为异常的抛出点,重复上述过程,直到异常被某catch子句捕获, 执行catch子句内容,完成异常处理。
(1)检测异常try
格式:
try{ //异常检测
…… //可能产生异常的代码
throw 异常值; //(1)抛出异常:异常值或异常对象
//function(); //(2)调用可能会抛出异常的函数
}
catch (异常类型1) //捕获异常
{
进行异常处理的语句
}
catch (异常类型2)
{
进行异常处理的语句
}
(2)抛出异常throw
格式:
throw 表达式;
说明:
throw类似于return语句,throw后的表达式可以是常数、变量或对象。 这里的表达式类型比表达式值更重要,因为在catch结构捕获异常时,是针对抛出异常类型进行判断,如果类型一致,则catch捕获该异常,否则,再去后续的catch块中重新进行匹配查找。
例如:
throw 1; //抛出int类型的异常,数值为1
throw (“异常”); //抛出char *类型的异常,内容为“异常”
说明:
catch块中是通过异常值类型完全匹配,因此,若想通过throw对抛出的多个异常分别进行处理,需要throw后加不同类型的表达式,而不能根据 表达式的不同数值来区分不同类型的异常。
例如:下面代码抛出的异常将被同一个catch块处理
throw 1; //抛出值为1的int类型的异常
throw 2; //抛出值为2的i nt类型的异常
(3)捕获异常catch
抛出异常后,程序的控制权将被异常类型匹配成功的catch块获得,catch块用于处理异常,也称为异常处理器。
格式:
catch (异常类型1声明) //捕获异常
{
进行异常处理的语句
}
……
catch (异常类型n声明)
{
进行异常处理的语句
}
说明:
try块对应的catch块会有多个,用于处理不同类型的异常。
catch后面括号中的异常类型声明可以是类型或对象声明,花括号中描述用于异常处理的代码。catch块的格式类似于函数定义,异常类型声明相 当于函数参数。
catch块必须直接放在try块之后,根据catch的排列顺序依次对抛出的异 常进行测试,若异常类型与某catch块的异常类型声明匹配,则进入该 catch块完成异常处理。
在catch块中可以定义局部变量,该变量在catch块外不可用。
(4)案例:除零异常的检测,抛出和捕获
# include <iostream>
# include <fstream>
using namespace std;
int main()
{
int x = 1, y = 0;
try //异常检测
{
if (y == 0)
{
cout << "除零异常" << endl;
throw 0; //抛出异常
}
else
{
cout << "无异常" << endl;
cout << x / y<<endl;
}
}
catch (int) //捕获异常
{
cout << "异常处理代码" << endl;
}
return 0;
}
输出结果
除零异常
异常处理代码
抛出异常和捕获异常的代码通常会封装在不同的模块中,因为包含抛出异常的 代码会实现某个功能,为了代码利用,将抛出异常的代码封装为独立的模 块,与捕获异常的代码实现分离。
将上面整除相除的操作封装成函数:抛出异常与捕获异常结构分离
# include <iostream>
# include <fstream>
using namespace std;
int int_div(int a, int b) //实现整数相除的函数
{
if (b == 0)
{
throw 0; //抛出异常
}
return a / b;
}
int main()
{
int x = 1, y = 0;
try //异常检测
{
cout << "检测异常" << endl;
cout << int_div(x, y) << endl;
}
catch (int) //捕获异常
{
cout << "异常处理代码" << endl;
}
return 0;
}
输出结果
检测异常
异常处理代码
(5)异常处理规则的强调
try块必须出现在前,catch紧跟其后出现,catch后的圆括号中必须含有数据类型,捕获是利用数据类型匹配实现的。
如果程序中有多个异常错误处理模块,则当异常发生时,系统自动查找与该异常类型相匹配的catch块,查找次序为catch出现的次序。
如果异常类型为C++的类,并且该类有其基类,则应将派生类的异常处理程序放在前面,基类的异常处理程序放在后面。
如果一个异常发生后,系统找不到一个与该异常类型匹配的异常处理模块,则调用预定义的运行时刻终止函数,默认是abort()
4,异常处理模式
异常处理模式——指处理完异常后,程序的执行流程。默认的异常处理模式为终止 模式。
终止模式——指异常抛出后,将退出导致异常的子程序或结构,转而去执行捕捉异常的catch块,catch块执行完毕后也不会再返回到抛出异常的位置。
以除零异常为例,观察终止模式的执行流程
# include <iostream>
# include <fstream>
using namespace std;
int int_div(int a, int b)
{
if (b == 0)
{
throw 0;
cout << "抛出异常" << endl;
}
return a / b;
}
int main()
{
int x = 1, y = 0;
try //异常检测
{
cout << "检测异常" << endl;
cout << int_div(x, y) << endl;
}
catch (int) //捕获异常
{
cout << "异常处理代码" << endl;
}
cout << "执行结束" << endl;
return 0;
}
输出结果
检测异常
异常处理代码
执行结束
执行流程分析:
若除数为0,则在int_div()函数中抛出异常,马上转去main()函数中相应的catch块进行处理,执行完catch块后,继续执行main()函数后续语句,并未再次返回int_div()函数执行throw后续内容。这种捕捉异常后,不再重新返回到抛出 异常处执行后续内容的规则,称为异常终止模式。
注意:不要把有效的、必须被执行的语句放在throw语句之后
5,异常规范说明
异常规范并不是C++中的强制规定,编译器并不会进行严格检查,但为了规范,通常应遵守规定,仅需简单了解即可。
由于抛出异常和捕捉异常的分离,在使用其他程序员编写的模块时,就需要了解模 块抛出的异常类型,方便在程序中编写相应的异常处理程序。C++提供了异常规范说明。异常规范说明就是在函数声明的形参表后,增加函数抛出异常类型的说明(即 列出该函数可能抛出的异常类型),方便函数的使用者编写相应的catch块来捕获 异常。
格式:
函数返回值类型 函数名(形参表)throw(异常类型1,异常类型2,…);
函数返回值类型 函数名(形参表)throw();
函数返回值类型 函数名(形参表);
说明:
第一种形式中,在throw后依次列出可能抛出的各种异常类型。
第二种形式,说明该函数不抛出任何异常。
第三种形式,形参表后没有表示抛出异常类型的说明,表示该函数可能抛出任 何类型的异常。
建议:
异常规范说明不是C++语法中的强制规定,因此第三种形式不会出现语法错误, 但为规范起见,建议采用前两种语法形式。
例如:可将上面的int_div()函数声明为:
int int_div(int a, int b) throw(int);
在使用函数指针进行函数调用时,可以对函数指针添加异常规范说明:
void (*pfun)() throw(int);
通过指针pfun进行调用的函数,只能抛出int异常。当带有规范说明的函数指针被初始化时,当作初值的函数,其异常规范中列出的异常类型数量应不多于函数指针异常规范中列出的异常类型。
例如:有如下函数指针声明和赋值语句:
void func1(int) throw(type1, type2, type3); //本函数可以抛出3种异常
void func2(int) throw(type1); //本函数可以抛出1种异常
void func3(int) throw(); //本函数不抛出异常
//声明函数指针变量pfun,通过该指针调用的函数可以抛出1种类型的异常
void (*pfun)(int) throw(type1);
pfun *func1; //错误,func1可以抛出3种类型异常,pfun指针要求只能抛出 1种异常
pfun *func2; //正确
pfun *func3; //正确
采用异常规范说明时,会将函数可能抛出的异常类型一一列出,但函数也有可能抛出没有出现在异常规范说明中的异常,若出现这种情况,一般编译可以通过,在程序运行过程中识别出该异常后,会执行库函数unexpected(),而unexpected()会调用terminate()函数,终止程序。
五,异常类
抛出异常除了抛出基本类型数据外,还可以是类对象,类中可以提供更多的异常信息。 定义异常类后,就可将该类对象作为要抛出的异常数据,操作的方法与抛出基本类型数 据相同。
案例:将上面的除零异常描述为一个对象
# include <iostream>
# include <fstream>
using namespace std;
class ZeroException
{
public:
ZeroException()
{
str = "除零异常";
}
string getstr()
{
return str;
}
private:
string str;
};
int int_div(int a, int b)
{
if (b == 0)
{
throw ZeroException();
}
return a / b;
}
int main()
{
int x = 1, y = 0;
try //异常检测
{
cout << "检测异常" << endl;
cout << int_div(x, y) << endl;
}
catch (ZeroException divzero) //捕获异常
{
cout << "异常是:" << divzero.getstr() << endl;
}
cout << "执行结束" << endl;
return 0;
}
输出结果
检测异常
异常是:除零异常
执行结束
六,捕获异常
1,重抛异常
若某个异常在处理过程中,发现需要更外层的结构对它进行处理,则可以在catch 结构中调用throw重新抛出异常,将当前异常传递到外部的try-catch结构中,这样 就允许多个处理程序访问该异常。重抛异常时只能从catch语句块或从catch块中 的调用函数中完成,该异常将不会被同一个catch捕捉,而是传递到外层的try-catch 结构中。
重抛异常的方法:
一是采用普通抛出异常的方式“throw 异常值或对象”重抛异常,
二是采用重抛异常的标准语法“throw;”完成异常重抛。
(1)采用普通方式重抛异常
在catch块中采用“throw 异常值或对象;”的方式重抛异常。
案例:除零异常,采用普通方式重抛异常:在catch块中采用“throw 异常值
# include <iostream>
# include <fstream>
using namespace std;
int int_div(int a, int b)
{
if (b == 0)
{
throw 0;
}
return a / b;
}
void exception_handler(int n1, int n2)
{
try
{
cout << "检测异常" << endl;
cout << int_div(n1, n2);
}
catch (int n)
{
cout << "捕获异常" << endl;
throw n;
}
}
int main()
{
int x = 1, y = 0;
try //异常检测
{
exception_handler(x, y);
}
catch (int) //捕获异常
{
cout << "再次捕获异常" << endl;
}
cout << "执行结束" << endl;
return 0;
}
输出结果
检测异常
捕获异常
再次捕获异常
执行结束
(2)采用标准语法重抛异常
标准语法格式:
throw; // throw后没有异常值或异常对象
说明:若抛出的异常是类对象,则通常采用标准语法。
例如:
定义异常类exception的派生类derive_exception:
class derive_exception: public exception
{
……
}
在内层try结构中抛出派生类异常:
try
{
try
{ …
throw derive_exception(); //抛出异常派生类对象
}
catch(const exception) //使用基类引用作为catch参数
{
cout<<”Can not handle the exception”<<endl;
throw e; //重抛基类对象异常
//throw; //采用标准语法重抛异常
}
}
catch (…) //处理异常
{ …
cout<<”Handle exception”<<endl;
}
存在的问题:内层try结构抛出了派生类对象异常,但接收异常的catch结构却使用了基类形参,catch结构中又将异常按照基类类型重新抛出,则会导致 原有派生类异常对象的部分丢失。
解决办法:采用异常重抛的标准语法格式重抛异常。
说明:采用标准语法抛出的是当前捕获的异常,而不是新的拷贝,采用“throw e”方式抛出的异常需要进行对象拷贝,拷贝动作带来了对象的构造与析构, 会降低工作效率。
2,捕捉所有异常
除了用于处理指定类型数据的catch结构外,还可以定义处理任意类型异常的catch 结构“catch(…)”,该结构应当出现在所有catch结构的最后,以处理与前面各个catch 结构不匹配的异常。
格式:
catch(…) //参数为“…”,不是具体的某种类型
{
异常处理语句
}
说明:
(1)捕捉所有异常的catch(…){ }结构,类似于swith中的default结构,它用来处 理与前面各catch结构都不匹配的剩余类型异常。
(2)若函数定义时没有异常规范说明,则有可能抛出任意类型的异常,此时可通 过增加一个能够捕获所有异常类型的catch(…)结构,来适配各种类型异常。
(3)catch(…)语句可以单独使用,也可以与其它catch语句一起使用,与其他catch 语句一起使用时,必须将catch(…)语句放在最后,否则,在碰到catch(…)后将终止 后续catch语句的匹配。
案例:除零异常
# include <iostream>
# include <fstream>
using namespace std;
int int_div(int a, int b)
{
if (b == 0)
{
throw 0;
}
return a / b;
}
int main()
{
int x = 1, y = 0;
try //异常检测
{
cout << "异常检测" << endl;
cout << int_div(x, y) << endl;
}
catch (int e) //捕获异常
{
cout << "除零异常:" << e << "整除0" << endl;
}
catch (...) //捕获所有异常
{
cout << "未知异常" << endl;
}
cout << "执行结束" << endl;
return 0;
}
输出结果
异常检测
除零异常:0整除0
执行结束
如果我们把int_div函数中的throw 0改成 throw "ok",就会捕获到未知异常了
七,标准库中的异常处理
C++标准库中提供了一组标准异常类,它们构成了以exception为基类的继承结构,程序 抛出的所有异常都派生自该基类。
基类exception定义在头文件exception中,它的接口如下:
class exception
{
public:
exception() throw();
exception(const exception &) throw();
exception &operator=(const exception &) throw();
virtual ~exception() throw();
virtual const char *what() const throw();
};
exception类接口中的函数都有一个空的异常规范throw(),表示exception类成员函数不 会抛出任何异常。成员函数what()返回一个字符串,它描述抛出异常的相关信息。what() 是一个虚函数,exception类的派生类可以重新定义what()函数,以便更好地描述派生 类的异常对象。
从基类exception可以直接派生出runtime_error(运行时异常)和logic_error(逻辑异 常),每个派生类又可以派生其他类。所有这些标准异常类可分为三组:
1,语言本身支持的异常:此类异常用于支持某些语言特性
new动态分配空间操作失败,会抛出bad_alloc异常
程序执行期间,若动态类型转换操作失败,dynamic_cast会抛出bad_cast异常
在执行类型辨别的过程中,若交给typeid(*p)的参数为零或空指针,typeid操作符 会抛出bad_typeid异常。
若发生意外异常(函数抛出异常规格(exception specification)以外的异常),通过 在函数的异常抛出表中添加std::bad_exception,会调用unexpected()抛出 bad_exception异常,而不是终止程序或调用set_unexception指定的函数。
2,C++标准程序库发出的异常:派生自logic_error(逻辑异常)
invalid_argument表示无效参数(参数异常)。
length_error指长度超过所操作对象允许的最大允许值(长度异常)。
out_of_range表示数组或下标之类的数值超过了界定的范围(越界异常)。
domain_error指出非法预处理错误(域异常)。
此外,标准程序库的IO部分提供一个名为ios_base::failure的特殊异常,当数据流 由于错误或者到达文件未尾而发生状态改变时,就可能抛出这个异常。
3,程序作用域之外发生的异常:派生自runtime_error(运行时异常)
表示程序中只能在执行时发生的错误:
range_error指出内部计算时发生区间错误(范围异常)。
overflow_error指算术运算时发生上溢(上溢出异常)。
underflow_error指算术运算时发生下溢(下溢出异常)。
为了使用异常,需要包含相应的头文件。bad_exception定义于<exception>。bad_alloc 定义于<new>。bad_cast和bad_typeid定义于<typeinfo>。ios_base::failure定义于 <ios>。其他异常类别定义于<stdexcept>。
4,特别的是
runtime_error和logic_error是一些具体的异常类的基类:
logic_error表示那些可以在程序中逻辑异常(被预先检测到的异常:若编写程 序时比较小心,则该类异常可以避免)[ˈlɒdʒɪk]逻辑、思维方式
runtime_error则表示运行时异常(难以被预先检测的异常)
这两个类及派生类均有一个接收“const string &”型参数的构造函数,在构造异常对象时,可将错误信息传递给该函数,之后通过调用该对象的what()函数,就可得到构造时的异常信息。
八,断言
1、断言概述
断言是一种用法简便的程序调试手段,可通过断言在代码发布前检测程序的逻辑是 否正确。
断言是对一般不可能发生的情况进行的判断,若断言情况为假,一般会终止程序。
编程调试时,通常会对程序代码做出一些假设,这些假设一般不应该发生,断言就 是在代码调试时捕捉这些假设。断言一般表示为布尔表达式,程序编写者相信在程 序正确时这些表达式的值为真,若表达式为假,则断言失败,执行相应的断言处理 动作(一般会调用abort()终止程序)。
断言在调试阶段使用,在项目发布时关闭,因此不会降低程序运行效率,使用断言 可以创建更稳定、安全性更高的程序。
2、断言的使用
在C++中,宏assert()用来在调试阶段实现断言。程序运行时,首先计算assert()宏 括号内表达式的值,若表达式为真,则程序继续执行后续内容;若为假,则报告错 误。
若想使用assert()宏,则需要包含头文件<cassert>。