文章目录
- 前言
- 一、异常
- 1.简单使用
- 2.注意事项
- 3.异常体系
- ①C++标准异常体系
- ②自定义异常体系
- 4.总结
- 优点
- 缺点
前言
是否知道C语言独特的错误处理方式——返回错误码,我们可以根据错误码来识别错误信息,比如识别了错误码,我们再用strerror函数把错误码对应的错误信息打印出来,于是便可知道哪错了。那C++是如何处理错误的呢?
一、异常
补充:C语言处理错误还可以用assert,不过在release版本下是不会弹出错误警告的,只会显示退出代码异常,因此assert是强调应该在调试期间,就把错误扼杀在摇篮中。
1.简单使用
- 先来感受一下异常的使用方式
double Division(int a, int b)
{
// 当b == 0时抛出异常
if (b == 0)
throw "Division by zero condition!";
else
return ((double)a / (double)b);
}
void Func()
{
int len, time;
cin >> len >> time;
cout << Division(len, time) << endl;
}
int main()
{
try
{
Func();
}
catch (const char* errmsg)
{
cout << errmsg << endl;
}
catch (...)
{
cout << "unkown exception" << endl;
}
return 0;
}
调试一下:
- 最终运行结果:
再来分析一下具体语法:
- try 大括号里面是可能存在异常的语句或者函数。
- catch 用于捕获可能存在的异常。
- throw抛出异常信息,通过catch进行捕获。
- 细节1:throw会抛给最近的栈帧的catch。
double Division(int a, int b)
{
// 当b == 0时抛出异常
if (b == 0)
throw "Division by zero condition!";
else
return ((double)a / (double)b);
}
void Func()
{
try
{
int len, time;
cin >> len >> time;
cout << Division(len, time) << endl;
}
catch (const char* errmsg)
{
cout << errmsg << endl;//
}
catch (...)
{
cout << "unkown exception" << endl;
}
}
int main()
{
try
{
Func();
}
catch (const char* errmsg)
{
cout << errmsg << endl;
}
catch (...)
{
cout << "unkown exception" << endl;
}
return 0;
}
这里发生异常就会抛给调用Division的函数Func,因为是最近的一层异常。
画张图总结一下:
-
细节2:catch的语法逻辑是跟if else if else 的逻辑一样的,是从最上面的catch,依次往下进行执行,如果存在合适的参数就进去,如果不存在就继续往下执行,直到碰到catch(…)——捕获任意类型的异常为止。如果还没有,就看下一层栈帧里面的catch的参数是否有所匹配,如果有就进去,或者再遇到catch(…)为止,如果到最后一层栈帧还没有,那就报错!
-
细节3:catch(…)——也就是最后一道防线,如果你要再进行throw,一定要判断下一层栈帧中是否也有catch(…)或者是否有参数匹配的。否则还是会报错的。
-
细节4:捕获异常之后,如果没有再次抛出的动作,那还按照当前栈帧之后的代码进行运行。
try
{
//要检查的可能会抛异常的语句或者函数。
//...
}
catch (const exception& e)
{
cout << e.what() << endl;
}
catch (...)
{
throw;//这里就是将捕获的异常进行再次抛出。
cout << "未知异常" << endl;
}
- 细节5:一次只能throw一次异常。
// 当b == 0时抛出异常
if (b == 0)
{
throw "Division by zero condition!";
throw 1;//是不会走这一句代码的!
}
总结:
- 可以抛任意类型的错误,但一定要能够捕获,不然是会报错的。
- 抛出异常是会给最近一层的栈帧进行处理的。
- catch的运行逻辑,类似与if else if else ,且走的是匹配的参数。
- catch(…)可以接收任意类型的参数,也可以抛给下一层有try catch的栈帧查看是否合适的参数,如果没有抛给下一层,那按照当前栈帧继续往下运行代码。
2.注意事项
- 在完成对象的初始化工作时,不要轻易抛出异常,否则可能会导致,对象的初始化工作,没有做完,可能之后使用的是一个不完整的对象。
- 在完成对象的资源清理工作时,也不要随意抛出异常,否则可能会导致内存泄漏等问题。
看这样一段代码:
int add(int n1, int n2)
{
return n1 + n2;
}
void func()
{
int* n1 = new int(0);
//当到这里出现异常时,只需抛出错误信息即可。
int* n2 = new int(0);
//当到这里出现异常时,我们不仅需要抛出错误信息,还得返回一个n1的指针进行资源的释放。
cout << add(*n1, *n2) << endl;
//当这里出现异常时,需要抛出错误信息,还得需要返回两个指针进行释放。
}
int main()
{
try
{
func();
}
catch (...)
{
}
return 0;
}
- 这是一件异常导致的很麻烦的事情,问题就先留到这里,等在智能指针时一并解决。
3.异常体系
①C++标准异常体系
- 说明:箭头的指向是父类。
- 注:蓝色方框的是常用的异常类型。
总结:
- 采用了继承的方式来达到异常的复用。
- 这样设计再加上多态可避免屎山代码。
- 返回的信息更加的全面而有针对性(通过类进行返回)。
举例:
int main()
{
try
{
//可能存在异常的代码
}
catch (const exception& e)
//这里实现了多态:通过基类的指针和引用
//来指向子类的对象。
{
cout << e.what() << endl;
}
//后面无需再列一串的代码进行匹配,只需要最后一道防线即可。
catch (...)
{
cout << "unknown exception" << endl;
}
return 0;
}
- 异常规范
// 这里表示这个函数会抛出A/B/C/D中的某种类型的异常
void fun() throw(A,B,C,D);
// 这里表示这个函数只会抛出bad_alloc的异常
void* operator new (std::size_t size) throw (std::bad_alloc);
// 这里表示这个函数不会抛出异常
void* operator delete (std::size_t size, void* ptr) throw();
// C++11 中新增的noexcept,表示不会抛异常
thread() noexcept;
thread (thread&& x) noexcept;
- 缺点1:不是强制的,可以不使用,因为兼容C语言,因此无法强制。
- 缺点2:C++98的throw可能会过于繁琐,如果抛出的异常类型过多,写起来很烦。
②自定义异常体系
- 在实际应用当中我们也是根据C++标准异常体系来用的,不过库里的显然不符合所有场景的需求,因此我们需要根据实际的需求来自己写一个异常类(符合实际需求的)但都得继承一下exception,方便统一进行捕获。
4.总结
优点
- 相比较C语言的返回值和断言的错误,异常能跳过多层栈帧,并且返回的错误信息更加的准确。
- 一些常见的库里面也存在异常,因此具有兼容性和移植性。
- 模板函数的异常通过返回值是无法处理的,抛异常能更好地解决这类问题。
缺点
- 因为可能会跳过多层栈帧,因此可能会打乱执行流,也就是说发生错误了,按照正常逻辑顺序,打的断点可能会直接跳过。
- 异常通常是以对象形式进行返回的,可能开销有点大,不过也只是有点而已,CPU很快,影响可以说忽略不计了。
- 异常的执行流乱跳,可能会导致内存泄漏的问题,具体样例看上面的代码。解决方法RAII,智能指针部分我们统一解决。
- C++的标准异常体系定义的太简陋了,因此会导致各个公司都有一套自己的标准体系。
- 尽量按照规范进行抛异常,因为一般异常都是在一个固定的区域进行捕获的,乱抛异常可能会导致无法定位异常的准确位置的问题。不过这个东西,C++并没有强制,不过规范是为了更为安全的使用和书写代码,因此还是按照规范来吧。