C++异常处理
写此篇文章是为了下一篇有关智能指针文章的铺垫。顺便好好总结一下学习以来的遇到的异常报错问题,后续还会跟进更新。
文章目录
- 语法
- 异常的抛出和匹配原则
- 异常安全问题
- 异常使用规范
- 自定义异常体系
- 异常的优缺点
进入正题
首先要了解为什么要处理异常,首先我们都不能保证自己所写出的所有代码都是绝对正确的,当发生错误时,虽然现在编译器大多时候都可以将代码中的问题呈现出来,但是难免会出现编译器也无法解释的情况,在之前编译器功能并没有这么强大时,就有聪明的人发明了异常处理机制,此时我们可以利用异常处理操作来方便我们找到错误发生的地方。同时更加体现我们严谨的态度,提高代码的严谨性。
还需要注意的是,C++不像JAVA,C#等有垃圾回收机制,所以这门语言在使用动态内存管理部分就要多加注意,避免产生内存碎片。就像十几年前刚盛行的安卓机,用一段时间就会变卡,这就是因为当时软件水平不高,使用的很多软件可能会不断产生内存碎片,只需要重启就不卡了。
从传统的异常处理手段深入学习
int main()
{int* a = 0;assert(a);//判断错误中止程序cout << *a << endl;return 0;
}
在学习C语言时,我们通常使用返回值或者断言来处理错误,也可能会选择打印一段字符串来呈现错误,异常处理就是利用这个想法,多了一些语法规定帮助我们更好使用。
语法
首先就是三个关键字,分别是throw,catch,try。
throw:出现问题时,就可以通过throw抛出异常。
catch:在想要处理可能出现的问题时,可以用catch捕获从某处抛出的异常,我们可以选择不同的接收对象,这是因为抛出的异常可能是数字,字符串,或者一个类。
try:后边跟着多个catch模块,和catch配合使用,就像if和else一样。
如下:
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
#include <assert.h>
using namespace std;
//今天复习的是异常方面,错误处理机制
double Division(int a, int b)
{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* errmg)//匹配该catch字段{cout << errmg << endl;}catch (...){cout << "unkown exception" << endl;}return 0;
}
如果我们运行程序后,将分母输入0,就会发生除0错误,运行后就会打印出DIvision by zero condition!告诉我们是发生了除零错误。如下图
异常的抛出和匹配原则
- 异常是通过抛出对象的类型来判断激活哪个catch处理代码的(如上例字符串类型)。
- 被选中的处理代码使距离函数调用链中与该抛出对象类型匹配且距离最近的,如上例被main函数中第一个catch捕获
- 抛出异常对象,就像函数参数一样,其实是一个异常对象的临时拷贝,是临时对象。
- catch(…)可以捕获任何一种类型的异常,通常是用来兜底,如上例代码中如果没有第二个catch,就会打印"kown exception"。
- 实际抛出和捕获匹配原则有一个例外就是并不是类型需要完全匹配,可以抛出派生类对象,用基类对象进行捕获。
异常安全问题
异常相当于更大范围上的if else,如果发生错误会直接激活catch,不会执行当前函数接下来的步骤,所以可能会出现内存泄露问题。
要注意的是
- 尽量不要在构造函数中抛出异常,否则可能导致对象没有正确初始化。
- 尽量不要在析构函数中抛出异常,否则会导致资源泄露,句柄未关闭等。
- 异常使用一定要足够谨慎,再new和delete之间使用可能导致内存泄露,在lock和unlock之间抛出异常会导致死锁,C++使用智能指针解决这个问题,参见智能指针这篇文章。
异常使用规范
异常规格说明是为了提高代码的可读性,让函数使用者知道调用的函数可能抛出哪些类型的异常,在函数后边加"throw(类型)",表示出函数可能抛出的异常。
如果括号中内容是空的,表示该函数不会抛出异常。
如果函数后什么都没加,说明这个函数可能抛出任意类型的异常。
C++11中新增关键字noexcept,加在函数后边表示函数不会出现异常。
如图
hhh能看的出这篇文章拖了很久,换了更好看的背景颜色。
自定义异常体系
虽然C++如今也有自己的异常体系,但是由于在此之前很多公司已经有了自己的一套比较成熟的异常体系,所以在工作时我们会使用自己公司的自定义异常体系,我们今天来学习如何自定义出一套异常处理机制。
首先来看C++的异常体系
std::exception: 所有标准异常类的基类,定义了what()函数,返回一个描述异常的C字符串。
std::bad_alloc: 当内存分配失败时抛出。
std::bad_cast: 当类型转换失败时抛出。
std::bad_typeid: 当使用typeid操作符操作一个空指针时抛出。
std::logic_error: 逻辑错误,可以在编译时检测到。
std::runtime_error: 运行时错误,只能在运行时检测到。
std::out_of_range: 当访问超出有效范围的数组元素时抛出。
std::invalid_argument: 当函数接收到无效参数时抛出。
在实际使用时,我们可以继承基类即exception类,可以通过重写what函数来实现。如下边的例子
class Exception //基类
{
public:Exception(const string& errmg, int id):_errmg(errmg), _id(id){}virtual string What() const{return _errmg;}
protected:string _errmg;int _id;
};class zeroException //内存异常
{
public:zeroException(const string& errmg, int id):_errmg(errmg), _id(id){}virtual string What() const{string str = "CacheException";str = _errmg;return str;}
protected:string _errmg;int _id;
};class httpException //链接异常
{
public:httpException(const string& errmg, int id):_errmg(errmg), _id(id){}virtual string What() const{string str = "httpException";str = _errmg;return str;}
protected:string _errmg;int _id;
};
int calculate(int a, int b)
{if (b == 0){throw zeroException("hhh",10);}else{return a / b;}
}bool link(int httpid)
{if (httpid != 10){throw httpException("error", 101);}else{return true;}
}int main()
{try{calculate(10, 0);link(1);}catch (const Exception& e){cout << e.What() << endl;}catch (...){cout << "Unkown Exception" << endl;}return 0;
}
上述代码中定义了两个子类,分别是除零错误还有连接异常的类,当然我们可以通过修改子类的成员函数和变量来创造出更加丰富多彩的类,从而满足我们自己的要求。
异常的优缺点
优点:
异常对象已经定义好了,相比C语言的方法可以更加清楚准确展示错误的信息,包含堆栈调用的信息,可以更好帮助我们定位程序bug。
相比于传统的返回错误码的方式,传统方式的最大特点就是调用深层函数出错就要层层返回错误,最外层才能拿到信息。
很多第三方库都包含异常,如boost,gtest,gmock等。
部分函数使用异常更好处理,如构造函数没有返回值,不方便使用错误码方式处理信息,如重写操作符operator这样的函数,pos月结只能使用异常或者终止该程序,没办法通过返回值表示错误。
缺点:异常会导致程序流乱跳,非常混乱,导致我们跟踪调试分析程序时比较困难。
异常会有一些性能开销。
C++没有垃圾回收机制,开出的空间需要自己进行管理,异常很容易导致内存泄露,死锁等问题,可以利用RAII即智能指针解决。
C++异常标准定义的不好,导致大家个自定义各自的标准体系,导致比较混乱。
异常使用尽可能规范,随便抛异常会使外层捕获函数难以处理,但是总体而言利大于弊,工程中还是推荐使用异常。