目录
1.c语言传统处理错误方式
1、终止程序
如assert断言、内存错误(原因有很多,越界访问啊等等)、除0错误
2、返回错误码
错误码,常见的就是程序运行后,如果没有错误的话,就是返回0,不正常的时候返回负的多少或者其他奇怪的数字。很多库的接口函数都把错误码放到errno中,表示错误。遇到这种,错误的原因需要查表或者自行寻找到错误原因。
2.c++异常概念
异常在很多面向对象的语言都有,python也有。
异常是一种处理错误的方式,可以理解为程度比assert轻一点,处理方式不是粗暴的终止程序。当出现某个编译器无法处理的错误时,可以将其作为异常抛出,由程序员预先设置的程序来处理这个错误。
**throw:**抛出异常,"throw 字符串或数字"
**catch:**捕获异常,比如捕获到了字符串或数字,就会进入对应的块域中,执行设定的语句。可以多个catch捕获多个异常(比如针对字符串的、针对数字的)
**try:**在try里面的语句,都会在运行时进行检查,如果遇到异常,把异常抛出后,会马上跳到catch中。
cpptry { // 保护的标识代码 }catch( ExceptionName e1 ) { // catch 块 }catch( ExceptionName e2 ) { // catch 块 }catch( ExceptionName eN ) { // catch 块 }
cppvoid fa() { int a; cin >> a; if (a == 2) { throw "wdawd"; } else if (a == 3) { throw 4; } else if (a == 4) { string s("dwwdawda"); throw s; } else if (a == 5) { stack<int>s; s.push(2); throw s; } } int main() { try { fa(); } catch (const char* err) { cout << err << endl; } catch (int x) { cout << x << endl; } catch (const string& es) { cout << es << endl; } catch (...) { cout << 231231 << endl; } return 0; }
如果没有遇到异常,在执行完try里面的语句后,会直接跳过catch里面的内容。
注意,如果是库里的对象返回的话,建议用const 类型&来接受,因为这样如果这个对象有移动拷贝的话,这个写法可以接受右值也可以接受左值,提高效率。
3.异常的使用
3.1异常的抛出与捕获
相比c语言传统的错误码(遇到错误,要层层返回,每层都要处理,直到返回main函数),异常可以直接跳回到匹配的catch处。这个过程中,跳过的函数都是会正常销毁的
异常抛出和匹配原则
1、异常是通过抛出对象引出的,该对象的类型决定了应该激活哪个catch
2、被选中的处理代码是调用链 (就是从main到第一个函数到其内的函数再到其内的函数,一路下去)中**与对象类型匹配且距离抛出异常的位置最近的,**比如在第一个函数处也有try,然后执行try里面的内容时抛了异常,并且catch的内容与抛出的对象的类型是匹配的,那就会执行第一个函数处的对应catch而不是main函数处的catch。
3、catch(...)可以捕获任意类型的异常,问题是不知道异常是什么 用来兜底的,建议一定加
4、抛出异常后,对于相应的对象,会生成该对象的拷贝 ,因为这个对象可能是个临时对象 (比如局部变量),这个拷贝出来的对象,在catch之后会销毁。(效率还是很高的,因为临时对象的话,走移动拷贝,效率挺高的)。比如我们抛出了一个string对象,就上面的代码中一样。
5、类型不一定严格匹配,可以抛出派生类对象,让基类捕获,利用多态。具体看下面的异常体系
在函数调用链中异常栈展开匹配原则
1、throw抛出后,要确认自己是否在try里面 ,在的话才去找匹配的catch,然后跳到匹配的catch处
2、没有匹配的catch,就跳出当前的函数栈 ,在调用该函数的函数栈中继续找匹配的catch
3、如果在main函数中都没有找到 匹配的catch就会终止程序 。而整个沿着调用链向上找匹配的catch的过程,就是栈展开。所以实际中,我们都会加个catch(...),否则有异常没捕获就会直接终止程序。
4、在匹配的catch处执行完对应的语句后,会沿着当前的try ....catch之后的语句继续执行。
3.2异常安全(还有一些异常重新抛出)
异常安全的核心问题就是在调用链非常长的情况下,异常会直接跳到catch,而这个过程很可能横跨了很多个函数。
1、不要在构造函数里抛异常,因为抛异常会直接跳到catch。如果构造函数里面要进行初始化,最典型的就是开辟多段空间给多个指针变量,如果这个过程中抛异常了,导致有些指针变量没有被初始化,仍然是野指针或空指针,那么析构函数在delete或者free的时候就会抛异常或者报错。简单点就是说可能会造成对象不完整或者没有完全初始化
2、析构函数不要抛异常。因为析构函数负责资源的清理,如果在析构函数中抛异常,很可能导致内存泄漏等问题,比如某个空间没有被delete掉。
3、异常经常会导致内存泄漏的问题。比如**new和delete中间有一块抛出了异常,**导致直接跳到catch了,delete就不会执行,这样就内存泄漏了,还会导致lock和unlock之间的死锁问题(这个是线程的知识,看我linux和网络的部分即可)
接下来是一种处理方法,利用异常重新抛出
cppvoid fa() { int a; cin >> a; if (a == 2) { throw "wdawd"; } else if (a == 3) { throw 4; } else if (a == 4) { string s("dwwdawda"); throw s; } else if (a == 5) { stack<int>s; s.push(2); throw s; } } void f() { int* x = new int; try { fa(); } catch (...) {//不一定是...,只是最保险的就是这个,不管抛了什么异常都可以接受 delete x; throw;//不管接受到什么异常,都通通重新抛出去。 } } int main() { try { f(); } catch (const char* err) { cout << err << endl; } catch (int x) { cout << x << endl; } catch (const string& es) { cout << es << endl; } catch (...) { cout << 231231 << endl; } return 0; }
核心思路就是在关键位置提前捕获,然后再重新抛出
比较恶心的情况:
cppvoid f() { int* x = new int; int *x1,x2; try { x1 = new int; try { x2 = new int; } catch (...) { delete x1; delete x; throw; } } catch (...) { delete x; throw; } delete x; delete x1; delete x2; }
这个方法写起来太麻烦了,可见这个方法也不是很好,依靠智能指针可以更加的高效处理这个问题
cppclass SmartPtr { public: SmartPtr(int *ptr) :_ptr(ptr) {} ~SmartPtr() { delete _ptr; } private: int* _ptr; }; void f() { SmartPtr s1(new int); SmartPtr s2(new int); SmartPtr s3(new int); //这样就可以依靠析构函数自动清理资源了。 //就算抛了异常,s1、s2、s3是局部变量,在出了作用域会自动销毁,调用它的析构函数 }
3.3异常规范
出于上面的情况,98的时候,希望大家写异常的时候规范一些。
cpp// 这里表示这个函数会抛出A/B/C/D中的某种类型的异常 void fun() throw(A,B,C,D); // 这里表示这个函数只会抛出A的异常 void* operator new (std::size_t size) throw (A); // 这里表示这个函数不会抛出异常 void* operator delete (std::size_t size, void* ptr) throw(); 单单靠上面3个,还是会出问题,比如写了抛abc,但实际上可能抛了d。 又比如函数深层调用,一层调一层那岂不是每一层函数都要写好多的throw()。 而且因为考虑到兼容c语言的问题,这只能是个建议而不是规定。 // C++11 中新增的noexcept,表示不会抛异常 thread() noexcept; thread (thread&& x) noexcept; //通过这个简化,只标记不会抛异常的函数。 但要小心的是,间接层加了noexcept,但这层函数调用的函数抛了异常,是不会检测到的。 而且一旦检测到抛出了规定外的异常,编译器也只是报错。
4.自定义异常体系
在实际过程中,大家都是设计一个异常体系,否则的下不同组各自管自己抛异常,最后害惨了外面管异常的,因为catch(...)是不确定究竟是什么异常,所以通过设计异常体系,让异常的信息更加的准确,方便处理。
接下来是一个比较常见的异常体系
cppclass Exception { public: Exception(const string& errmsg, int id) :_errmsg(errmsg) , _id(id) {} virtual string what() const { return _errmsg; //返回错误描述 } protected: string _errmsg;//错误描述,比如基础的错误信息,比如内存错误等等 int _id; //错误id }; //这里会有一个类,比如说是数据库开发组的。 class SqlException : public Exception { public: SqlException(const string& errmsg, int id, const string& sql) :Exception(errmsg, id) , _sql(sql) {} virtual string what() const { string str = "SqlException:";//表明自己是什么错误类的 str += _errmsg; str += "->"; str += _sql; return str; //再基础的错误信息基础上,加上自己的额外备注信息,让错误描述更加的准确 } private: const string _sql; }; //缓存组,具体做什么因人而异,主要是不同的组都会加上一串字符串,表示这个异常是 //哪个组负责的 class CacheException : public Exception { public: CacheException(const string& errmsg, int id) :Exception(errmsg, id) {} virtual string what() const { string str = "CacheException:"; str += _errmsg; return str; } }; //网络组的,记录请求类型等等的 class HttpServerException : public Exception { public: HttpServerException(const string& errmsg, int id, const string& type) :Exception(errmsg, id) , _type(type) {} virtual string what() const { string str = "HttpServerException:"; str += _type; str += ":"; str += _errmsg; return str; } private: const string _type; };
那么如何捕获呢?
cppcatch (const Exception& e) // 这里捕获父类对象就可以 { // 多态 cout << e.what() << endl; }
利用继承的特性,用父类对象的引用来接受派生类或者父类本身。
然后利用虚函数的特性,在满足多态的情况下,如果接受的派生类,那么what调用的就是派生类的what,如果接受的是父类,就会调用父类的what。
5.c++标准库的异常体系
c++标准库也是弄了一个异常体系。
图片出自菜鸟编程。
6.异常优缺点
1、优点
1、相比传统的错误码,异常可以更加清晰的展现出错误的信息,甚至包含堆栈的调用信息,这样可以帮助更好的定位bug
2、错误码必须通过层层的retrun返回错误码,然后才能拿到错误码,相比之下异常可以直接拿到信息,跳过中间的过程。
3、很多第三方的库都包含了异常,使用的时候也得关注异常。
4、部分情况用异常更好,比如构造函数没有返回值,只能抛异常。比如at这个函数,返回什么很重要,但问题是返回什么都可能不是错误信息,反而可能是一个正常的值,但确实下标越界了。
2、缺点
1、异常会导致程序的执行流乱跳,会非常混乱,尤其是运行时的时候抛异常就会乱跳。导致调试跟踪以及分析程序的时候会非常困难。
2、异常有一定的性能开销,不过在现在的硬件下,基本可以忽略不计。
3、c++没有垃圾回收机制,资源需要自己管理,容易造成内存泄漏、死锁等异常安全问题,想要解决,建议是rall(智能指针的内容)。
4、标准库的异常体系定义的一般,导致所有人都是各自定义各自的,比较混乱。
5、异常需要尽量的规范使用,否则负责捕获异常处理异常的人会很痛苦。主要是,所有的异常继承自一个基类;函数是否抛异常、抛什么异常,都使用func() throw()的形式规范
7、补充
异常一旦发生,说明程序出现了非法的情况
程序中只要有异常,就必须处理。一旦有抛出异常,那么必须捕获,否则代码最后会崩溃。
基类的const类型引用,可以捕获所有的子类的异常对象