您现在的位置是:首页 >技术教程 >【C++】异常,你了解了吗?网站首页技术教程
【C++】异常,你了解了吗?
简介【C++】异常,你了解了吗?
在之前的C语言处理错误时,会通过assert和错误码的方式来解决,这导致了发生错误就会直接把程序关闭,或者当调用链较长时,就会一层一层的去确定错误码,降低效率,所以c++针对处理错误,出现了异常,一起来学习!
目录
1.C++异常的出现
C语言中处理错误的方式
1.终止程序,如assert,缺陷:用户难以接受。如发生内存错误,除0错误时就会终止程序。
2. 返回错误码,缺陷:需要程序员自己去查找对应的错误。如系统的很多库的接口函数都是通
过把错误码放到errno中,表示错误实际中C语言基本都是使用返回错误码的方式处理错误,部分情况下使用终止程序处理非常严重的错误。
C++异常概念
异常是一种处理错误的方式,当一个函数发现自己无法处理的错误时就可以抛出异常,让函数的
直接或间接的调用者处理这个错误。
throw: 当问题出现时,程序会抛出一个异常。这是通过使用 throw 关键字来完成的。
catch: 在您想要处理问题的地方,通过异常处理程序捕获异常.catch 关键字用于捕获异
常,可以有多个catch进行捕获。
try: try 块中的代码标识将被激活的特定异常,它后面通常跟着一个或多个 catch 块。
如果有一个块抛出一个异常,捕获异常的方法会使用 try 和 catch 关键字。try 块中放置可能抛
出异常的代码,try 块中的代码被称为保护代码。
如果正常执行,没有发生异常,那就和try-catch没有关系了,就会正常进行。
举个例子:double Division(int a, int b) { // 当b == 0时抛出异常 if (b == 0) throw "Division by zero condition!";// throw const char*对象 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 (int errid) { cout << errid << endl; } return 0; }
可以看到,try中调用func函数,若调用这个函数过程中有异常抛出,就会直接跳到try下面的catch来捕获异常。那么异常具体要注意些什么,它的规则又是什么呢?
2.异常的抛出和匹配原则
1. 异常是通过抛出对象而引发的,该对象的类型决定了应该激活哪个catch的处理代码。throw抛出的异常,必须要有类型匹配的catch所对应,如果没有类型匹配的catch,会直接报错,终止程序!
2. 被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那一个。当除数为0时,抛出异常,他会先找当前作用域的catch,如果没有,就会返回调用链中调用它的上一个中去找catch,如果没有catch,就会直接报错,终止程序。
同一位置可以有多个类型不用的catch,但是同一位置不可以有相同类型的catch,相同类型的catch必须分布在不同的位置(可以看作是提前catch):
double Division(int a, int b) { // 当b == 0时抛出异常 if (b == 0) throw "Division by zero condition!";// throw const char*对象 else return ((double)a / (double)b); } void Func() { try { int len, time; cin >> len >> time; cout << Division(len, time) << endl; } catch (const char* errmsg) //const char*类型 { cout << errmsg << endl; // 记录日志,进行统一处理 } } int main() { try { Func(); } catch (const char* errmsg) //const char*类型 { cout << errmsg << endl; // 记录日志,进行统一处理 } catch (int errid) //int 类型 { cout << errid << endl; } return 0; }
throw抛出的可以是异常对象:3. 抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象,所以会生成一个拷贝对象,这个拷贝的临时对象会在被catch以后销毁。(这里的处理类似于函数的传值返回)当然,编译器会自动识别为将亡值,然后调用移动构造,直接构造。
4. catch(...)可以捕获任意类型的异常,问题是不知道异常错误是什么。如果出现这种情况:new出的对象因为catch而导致执行顺序混乱,没有释放就会导致内存泄漏。double Division(int a, int b) { // 当b == 0时抛出异常 if (b == 0) { string s("Division by zero condition!"); throw s; } else return ((double)a / (double)b); } void Func() { //new的资源需要释放 int* p1 = new int[10]; try { int len, time; cin >> len >> time; cout << Division(len, time) << endl; // throw const char*对象 // func() // throw double对象 } catch (...) // 表示可以捕获任意类型的异常 { //cout << errmsg << endl; cout << "delete" << p1 << endl; delete[] p1; throw; // 重新抛出,捕获到什么就抛出什么 } //catch (const string& errmsg) //{ // //cout << errmsg << endl; // cout << "delete" << p1 << endl; // delete[] p1; // throw errmsg; // 重新抛出 //} cout << "delete"<< p1 << endl; delete[] p1; } int main() { try { Func(); } catch (const char* errmsg) { cout << errmsg << endl; // 记录日志,进行统一处理 } catch (...) { cout << "未知异常" << endl; } return 0; }
那么就可以在catch捕获异常里面,释放内存以后,再重新抛出异常throw;(但不知道是什么类型) ,若无类型匹配就会直接停止程序,于是有 catch(...)可以捕获任意类型的异常,释放空间时,就可以直接在catch中写一次就可以。(但也不知道异常错误是什么)所以为了在main中统一处理异常,这就要要求抛异常得规范,使得捕获以后可以识别异常类型。(后面继续看)
5. 实际中抛出和捕获的匹配原则有个例外,并不都是类型完全匹配,可以抛出的派生类对象,
使用基类捕获,这个在实际中非常实用,后面会详细讲解。
异常规范:
// C++11
中新增的
noexcept
,表示不会抛异常
thread
()
noexcept
;
thread
(
thread
&&
x
)
noexcept
;
3.在函数调用链中异常栈展开匹配原则
1. 首先检查throw本身是否在try块内部,如果是再查找匹配的catch语句。如果有匹配的,则调到catch的地方进行处理。前面我们提到了,try中是保护代码,如果try中throw抛异常以后,首先看有没有对应的catch(类型匹配),没有的话直接报错,停止程序,有的话直接跳到catch来进一步解决。
2. 没有匹配的catch则退出当前函数栈,继续在调用函数的栈中进行查找匹配的catch。当然不是当前函数栈没有对应的catch就直接终止程序,会依次返回调用链的上一层去寻找,知道调用链结束,没找到就直接报错,停止程序
3. 如果到达main函数的栈,依旧没有匹配的,则终止程序。上述这个沿着调用链查找匹配的catch子句的过程称为栈展开。所以实际中我们最后都要加一个catch(...)捕获任意类型的异常,否则当有异常没捕获,程序就会直接终止。当然程序一般不会随便就结束,若没有对应的catch捕获异常匹配,为了防止程序结束,就会在最后加一个catch(...){},防止程序结束。
4. 找到匹配的catch子句并处理以后,会继续沿着catch子句后面继续执行。
4.异常的特殊类型匹配(异常体系)
实际使用中很多公司都会自定义自己的异常体系进行规范的异常管理,因为一个项目中如果大家
随意抛异常,那么外层的调用者基本就没办法玩了,所以实际中都会定义一套继承的规范体系。
这样大家抛出的都是继承的派生类对象,捕获一个基类就可以了。
这就有了用子类抛异常,父类来捕获异常。什么意思呢?来看例子:
//规定一个异常的标准类 class Exception { public: Exception(const string& errmsg, int id) :_errmsg(errmsg) , _id(id) {} virtual string what() const { return _errmsg; } protected: string _errmsg; int _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; }; void SQLMgr() { srand(time(0)); if (rand() % 7 == 0) { throw SqlException("权限不足", 100, "select * from name = '张三'"); } cout << "调用成功" << endl; } void CacheMgr() { srand(time(0)); if (rand() % 5 == 0) { throw CacheException("权限不足", 100); //throw 1; } else if (rand() % 6 == 0) { throw CacheException("数据不存在", 101); } SQLMgr(); } void HttpServer() { // ... srand(time(0)); if (rand() % 3 == 0) { throw HttpServerException("请求资源不存在", 100, "get"); } else if (rand() % 4 == 0) { throw HttpServerException("权限不足", 101, "post"); } CacheMgr(); } int main() { while (1) { Sleep(1000); try { HttpServer(); } catch (const Exception& e) // 这里捕获父类对象就可以 { // 多态 cout << e.what() << endl; } catch (...) { cout << "Unkown Exception" << endl; } } return 0; }
可以来看一下结果:
catch(...){}的作用就是方式其他无匹配的异常类型报错停止程序。
这里其实也用到了多态的调用,通过父类的引用,来调用重写以后的虚函数,从而实现多态调用。
这就很好的解决了问题。
C++ 提供了一系列标准的异常 ,我们可以在程序中使用这些标准的异常。但是实际中我们可以可以去继承 exception 类实现自己的异常类。但是实际中很多公司像上面一样自己定义一套异常继承体系。因为 C++ 标准库设计的不够好用。只需知道这些异常代表的意义:(申请内存空间)(越界访问)
5.异常安全
1.构造函数完成对象的构造和初始化,最好不要在构造函数中抛出异常,否则可能导致对象不
完整或没有完全初始化
2.析构函数主要完成资源的清理,最好不要在析构函数内抛出异常,否则可能导致资源泄漏(内
存泄漏、句柄未关闭等)
3.C++中异常经常会导致资源泄漏的问题,比如在new和delete中抛出了异常,导致内存泄
漏,在lock和unlock之间抛出了异常导致死锁,C++经常使用RAII来解决以上问题,关于RAII
会在智能指针进行讲解。(我们下期再见)
6.异常的优缺点
优点:
1. 异常对象定义好了,相比错误码的方式可以清晰准确的展示出错误的各种信息,甚至可以包含堆栈调用的信息,这样可以帮助更好的定位程序的bug。
2.
返回错误码的传统方式有个很大的问题就是,在函数调用链中,深层的函数返回了错误,那
么我们得层层返回错误,最外层才能拿到错误。但C++的异常可以直接跳转到捕获异常的位置。
错误码返回要层层判断,当前遇到错误,返回上一层要判断返回的错误码。
缺点:
1. 异常会导致程序的执行流乱跳,并且非常的混乱,并且是运行时出错抛异常就会乱跳。这会
导致我们跟踪调试时以及分析程序时,比较困难。
2. 异常会有一些性能的开销。当然在现代硬件速度很快的情况下,这个影响基本忽略不计。
3. C++没有垃圾回收机制,资源需要自己管理。有了异常非常容易导致内存泄漏、死锁等异常
安全问题。这个需要使用RAII来处理资源的管理问题。学习成本较高。
总之,利大于弊!
风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。