C++ assert函数与捕获异常
assert()函数
C语言和C++都有一个专门为程序调试准备的工具函数,这就是assert()函数,这个函数是在C语言的assert.h
库文件里定义的,所以包含到C++的程序里我们用以下语句对此头文件进行包含:
#include <cassert>
assert()函数需要有一个输入参数,他将测试这个输入参数的真or假状态:
如果这个输入参数的 状态为真,程序什么都不做
如果这个输入参数的 状态为假,程序按照要求进行动作
例如以下代码:
int main()
{
int i = 20;
assert(i == 65);//i此时不为65 所以此时assert函数中的输入参数i的状态为假
}
条件判断后的结果:
从上述代码的演示结果我们可以看到assert()函数可以帮我们调试程序,我们可以利用它在某个关键程序的关键假设不成立时立即停止该程序的执行并报错,从而避免发生更严重的问题。
除assert()函数之外,在程序的开发,测试阶段,我们还可以使用大量的cout语句来报告程序里正在发生的事情。
对程序异常和错误信息进行提示的原则:最终用户看到的错误信息应该即专业又清晰,不能轻易的中断程序,不能充满技术细节。
异常处理
为什么需要捕获异常:
人们设计出来的程序,是做不到天衣无缝的,在运行时总会出现各种无法进行预判的异常。因此, 我们需要这样一种机制:这种机制不仅能在程序正确的情况下正常运行,还能在程序出现错误的情况下进行相应合理的处理(合理的处理方式是 程序出现异常的时候抛出错误信息,等待解决方案 而不是让错误直接使程序莫名崩溃,甚至导致死机),为了解决这些问题,C++提供了异常处理机制,它一般是由try语句和catch语句构成。
同样是为了对付潜在的编程错误(尤其是运行时的错误,捕获异常是一种完全不同的方法)
捕获异常的基本语法:
C++异常处理涉及三个关键字: [try] [catch] [throw]
throw: 当问题出现(检测到错误),程序便会抛出一个异常,这是通过使用throw关键字来完成的
catch:在想要处理问题的地方,通过异常处理程序来捕获异常,catch关键字用于异常的捕获
try:try块中的代码标识着将被激活的特定异常,try语句块后面经常跟着一个或多个catch语句
捕获异常的基本使用思路:
1.安排一些C++代码(try语句)去尝试做某件事,尤其是哪些很可能会失败的事(比如打开一个文件或申请一些内存)。
2.如果发生问题,就抛出一个异常(throw语句)。
3.在安排一些代码(catch语句)去捕获这个异常并进行相应的处理。
基本语法结构:
int func(double a,double b)throw(Exceptiontypename抛出的异常的类型1,抛出的异常的类型2) //声明一个具有异常抛出的异
常检测功能函数func
try //异常检测
{
Do something.
Throw an exception on error.
}
catch//异常捕获
{
Do whatever.//异常处理
}
throw语句:
C++通过throw语句来决定抛出的异常信息的类型和需要进行异常捕获的函数的位置,throw语句一般放到需要进行异常捕获的函数声明后方,throw语句的语法如下:
throw(抛出异常表达式) 可以是自定义类型的表达式
该语句抛出一个异常,异常可以是一个表达式,其值的类型可以是基本类型,也可以是自定义的类型。
在某个try语句块里执行过throw语句,throw语句后面的所有语句(范围截止到这个try语句块的末尾)将永远不会被执行。
try…catch语句:
try //异常检测
{
语句组1;//异常检测目标语句
(如果此语句中有一个被进行了异常捕获的函数存在,那么try语句将判断此函数是否处于异常状态)
语句组2;//异常检测通过后执行的语句
程序检测无异常抛出后,才会执行此语句2, 若程序处于异常状态程序只会执行语句1,语句1以后的语句将不会被执行
}
catch(抛出的异常的类型1)//被throw抛出的异常的捕获
{
异常处理代码1 //对抛出的不同类型的异常信息 进行不同的处理
}
catch(抛出的异常的类型2)
{
异常处理代码2
}
因为一个程序总有着不同的的异常情况发生,有时候需要我们返回一个int类的错误信息,有时则需要返回一个char类型的字符进行错误提示,所以异常捕获语句catch可以有多个,但是至少得有一个catch语句,并且catch语句中的抛出异常的类型需要和throw中抛出异常的类型一致。
try…catch语句的执行过程是:
我们不妨把try和其后的{}代码块内容称作"try块",把catch和其后的{}中的内容称作"catch块"
★执行try块中的语句,如果执行{}块中的语句的过程中没有异常被抛出,那么执行完try语句块的内容就直接忽略后方的catch语句块,直接执行到最后一个catch语句块后的语句,所有的catch语句都不会被执行。
★如果try语句块的执行过程中有异常被抛出,那么抛出异常后立即跳转到第一个与"异常类型"和抛出的的异常类型匹配的catch块中执行(称作异常被catch块所"捕获"),执行完catch语句块中的异常处理代码后,再跳转到最后一个catch块后方继续执行。
以下列程序为例:
#include <iostream>
using namespace std;
double division(double a, double b)//给division这个函数设置异常抛出
{
if (b == 0)//触发异常的条件
{
throw 0;//抛出一个int类的异常
}
if (b == -1)
{
throw "抛出异常:被除数为负数\n";//抛出类型为char的异常信息
}
else
{
return a / b; //如果没有触发异常 执行正确的函数流程
}
}
int main()
{
double a, b;
while (cin >> a >> b)//输入 a b两个数
{
try//异常测试
{
cout << division(a, b)<< endl; //调用 division这个带有抛出异常throw的函数 检测其中是否有异常被抛出
cout << "程序未捕获到异常" << endl; //此句在未有异常抛出的情况下才会被执行
}
//检测到到抛出的异常信息
catch (int error)//捕获异常 被捕获的异常类型为 int类的error
{
cout << "抛出异常:被除数为" <<error<< endl;//异常处理代码
}
catch (const char* error)//捕获 抛出的异常类型为 char * 类的 异常error
{
cout << error;//异常处理代码
}
catch (...)//能捕获任何类型异常的catch块
{
cout << "程序出现异常";
}
cout << "\n程序结束" << endl;
//未检测到有异常信息
//执行完try语句块后的语句后从此处开始执行
cout << "程序结束" << endl;
}
return 0;
}
运行结果:
当被除数"b"为0的时候,try块会抛出一个整型的异常。抛出异常后,try块立即停止执行。该整型异常会被类型匹配的第一个catch块捕获,及进入catch(int error)块执行,该catch块执行完毕后,程序才会继续往后执行,直到程序正常结束。
如果抛出的异常未能被catch块捕获,比如将catch块(catch(int error))改为catch(double error),那么当被除数b为0的时候,抛出的整型异常就没有catch块能捕获,这个异常也就得不到处理,那么程序会立即中止并报错,try…catch后面的内容都不会被执行。
能够捕获任何异常的catch语句
如果希望不论抛出那种类型的异常都能被catch块所捕获,可编写如下catch块:
catch(...)
{
...
}
这样的catch块能捕获任何被抛出的还没有被捕获的异常,如何它的前面存在一个可以捕获当前抛出的异常的catch块,则catch(…)不会被执行。
由于catch(…)能匹配任何类型的异常,这会导致他后面的catch块实际上就不起作用,因此不要将他写在其他catch块前面。
异常的再抛出
如果一个函数在执行过程中抛出的异常在本函数内就被函数内设置的catch块捕获并进行处理,那么该异常就不会抛给这个函数的调用者(也称 “上一层的函数” 和"调用函数"),如果异常在本函数中未被捕获并处理,那么它就会被抛给上一层函数。
例如以下程序:
class CException
{
public:
string msg;
CException(string s) : msg(s) {} //自定义类
};
double Devide(double x, double y)//除法函数Devide
{
if (y == 0)
throw CException("devided by zero");//抛出自定义的CException类的异常
cout << "in Devide" << endl;
return x / y;
}
int CountTax(int salary) //异常检查函数CountTax
{
try {
if (salary < 0)
throw - 1; //如果传入的实参salary为负数 则抛出iint类的异常
cout << "counting tax" << endl;
}
catch (int) {//捕获int类型的抛出异常
cout << "salary < 0" << endl;
}
cout << "执行计算" << endl;
return salary * 0.15;
}
int main()
{
double f = 1.2;
try {
CountTax(-1); //乘法函数
f = Devide(3, 0); //除法函数
cout << "try语句块执行完毕" << endl;
}
catch (CException e)
{ //捕获CException类型的抛出异常
cout << e.msg << endl;
}
cout << "f = " << f << endl;
cout << "finished" << endl;
return 0;
}
CountTax函数抛出的异常
如果CountTax乘法函数抛出了异常,且这个异常在CountTax乘法函数中被catch语句块捕获并处理,那么CountTax乘法函数里的这个异常就不会被抛给调用者(即main函数),因此在main函数的try语句块中,CountTax之后的语句还能正常执行。
Devide函数抛出的异常
Devide函数如果抛出了异常却不进行处理,该异常就会被抛给Devide函数的调用者,即main函数,抛出此异常后Devide函数立即结束, 此时[return x / y;
] 此句不会被执行,函数也不会返回一个值
Devide函数中抛出的异常被main函数中类型匹配的catch块捕获,catch块捕获的异常类型是用户自定义的类型CException 类的,[catch (CException e)
]中的e对象是利用复制CException 的构造函数进行初始化的.
在catch块中抛出异常
在某些情况,虽然我们在函数内对异常进行了处理,此时如果不在catch块内继续抛出异常,调用者catch块将捕获不到任何异常,但此时还是希望能够通知一下调用者(例如 main函数),以便于做进一步的处理,这就需要我们在函数内catch块中继续抛出捕获到的异常,例如:
try
{
......
}
catch (string s )
{
cout << "CountTax error : " << s << endl;
🔥 throw; //继续抛出捕获的异常
}
🔥[throw;]没有指明抛出的异常的类型,因此抛出的就是catch块捕获到的那个异常。
函数的异常声明列表
为增强程序的可读性和可维护性,使程序员在使用一个函数的时候就能看出这个函数可能会抛出哪些异常,C++允许在函数声明和定义的时候,在函数声明后加上它所能抛出的异常的列表
具体的写法如下:
void func()throw(int,double A,B,C);//函数声明
void func()throw(int,double A,B,C) //函数定义
{}
上面的写法表明 func 可能拋出 int 型、double 型以及 A、B、C 三种类型的异常。异常声明列表可以在函数声明时写,也可以在函数定义时写。如果两处都写,则两处应一致。
如果异常声明列表如下编写:
void func() throw(); //即异常列表中没有任何类型的异常
则说明此func函数不会抛出异常, 一个函数如果不交代能抛出哪些类型的异常,就可以抛出任何类型的异常,所以函数也可以不加异常声明列表。
注意:🎯
1.每条try语句至少要有一条配对的catch语句,必须定义catch语句以便让他接受一个特定类型的参数。
2.如果抛出的异常是派生类的对象,而catch的异常类型是基类,那么这两者也会相匹配,因为派生类对象继承了基类对象
3.catch和try后面执行的语句都必须用花括号包含,即使只有一条语句。在两者之间不得插入其他语句
4.要把可能出现异常的,要检查的语句放在try后面的花括号中,或者说有throw关键字抛出异常的程序段
5.可以只有try而没有catch
6.catch后面的参数,一般是一个抛出异常的类型就可以了。"…"删节号表示所有类型
7.异常信息提供给try-catch语句结构,系统会去寻找与try匹配的catch语句,try和catch是成对出现的,类似if和else
为什么需要异常处理机制:
1.与使用一个[if-else]条件语句或return语句相比,采用异常处理机制的好处是可以把 程序的正常功能与逻辑部分和程序出错的功能与逻辑部分 进行清晰的划分而不是让他们混杂在一起
2.虽然函数也可以通过返回值和传引用的的参数来通知调用者程序发生了异常,但采用这种方法,每次调用函数都需要判断是否发生异常,这个函数被多处调用时比较麻烦,有了异常处理机制,可以将多处函数调用都写到一个try块中,任何异常调用发生异常就会被匹配的catch捕获并处理,也就不需要每次调用都判断是否发生异常
3. 异常处理机制体现了面对对象程序设计的思想,以判断被除数是否为0这个例子,加入我们只用if-else语句,逻辑上是可以保证程序的正常运行,但是异常处理的方式就被固定了,比如处理方式是打印出 [“警告,被除数为0”] 这样的一个警告,那么所有调用我divide这个函数的程序,在遇到被除数为0的这种异常情况下,处理的方式都是打印出 [“警告,被除数为0”] ,这不符合面对对象的代码重用思想的,因为我设计出的这个divide函数是封装在程序里不能被别人使用的。比如现在我又想设计一个已知路程和时间,求平均速度的程序,如果我想弹出警告为[“时间长度不能为0”]。那么我之前设计的divide函数就用不到了,但如果divide函数的作用是抛出一个异常,我就能接收这个异常并用我自己的方式去处理这个异常。 也就是说只在程序发生错误的时候只是抛出一个异常信息,不对异常进行直接处理,把具体怎么处理这个异常的决定权交给调用函数的人,而不是在函数内自行进行决定。让调用者知道此函数有一个异常存在
4. 减少代码冗余,使程序更简洁。从理由1中可以看到,我们可以重用函数divide,就意味着减少冗余的代码。或者你可能还会质问,如果我在调用divide函数之前,做分母是否为0的判断,那么依然可以正确的调用divide函数,在遇到分母为0的情况,就不调用divide函数,而是打印出自己想要的报错信息或者处理方式。可以告诉你,显然这样会增加工作量,因为每次你调用divide函数时,都要做一个异常判断,我们在实例中的异常判断很简单,只是if(b==0)就可以了,但其他的一些复杂的逻辑,可能要用到大量的代码去检查出异常。如果每次调用divide这样一个函数,我们都要做一次异常判断,那岂不是使得程序很复杂冗余?divide函数是不是设计的很不合理?
5. 一般情况下,如果项目不是很大,或者说我们设计出来的方法不会被用于各种场合,那么直接使用C++自带的异常处理就可以了,把可能发生错误的代码段放入try中,然后catch中做相应处理。这是我个人认为,极端的假设,如果程序很小,if和else更有优势, 因为没有涉及到代码重用的情况下,try–catch根本没有它们发挥的地方
总结:
1.应该只用异常来处理确实可能不正常的情况。
2.在构造器和析构器里不应使用异常,使用异常稍有不慎就会导致严重的问题。
3.如果try语句无法找到一个与之匹配的catch语句,他抛出的异常将终止整个程序的运行。
4.C++标注库中有一个名为exception的头文件,该文件声明一个exception的基类,可以用这个基类来创建个人的子类以管理异常
来自:
http://c.biancheng.net/view/422.html
https://www.runoob.com/cplusplus/cpp-exceptions-handling.html
https://blog.csdn.net/Dengdew/article/details/80617344