【C++】异常介绍
🎆个人主页:夜晚中的人海
今日语录:梦想,可以天花乱坠,理想,是我们一-步一个脚印踩出来的坎坷道路。 —— 三毛
文章目录
- ⭐一、异常的概念
- 🎄二、栈展开
- 🎆三、异常的抛出和捕获
- 🚀四、异常的重新抛出
- 🎉五、异常的匹配原则
- 🚘六、异常的安全问题
- 🎡七、异常规范
- 🏝️总结(优缺点)
⭐一、异常的概念
概念:异常是一种处理错误的方式,当一个函数出现自己无法处理的错误时,会抛出异常,然后让函数直接或间接的调用者来处理这个错误
在C语言中,主要通过错误码的形式来处理错误,而错误码的本质就是对错误信息进行编号,拿到错误码以后还要去查询错误信息,这样的做法会比较麻烦。
使用异常时有常见的三个关键字:
throw: 当一个函数出现问题时,程序就会抛出异常
try: 包含会出现异常的函数或者代码,后面一般会跟一个或多个catch模块
catch: 用来捕获异常
🎄二、栈展开
抛出异常后,程序会暂停当前函数的执行,开始寻找与之匹配的catch子句,首先先检查throw本身是否在try块内部,如果在则查找匹配的catch语句,如果有匹配的,则跳到catch的地方进行处理。
如果当前函数中没有try/catch子句,或者有try/catch子句但是类型不匹配,则退出当前函数,继续在外层调用函数链中查找,这种查找的方式叫做栈展开
如果到达main函数,依旧没有找到匹配的catch子句,程序会调用标准库的 terminate 函数终止程序;如果找到匹配的catch子句处理后,catch子句代码会继续执行
🎆三、异常的抛出和捕获
当程序出现问题时,我们会抛出一个对象来引发一个异常,而该对象的类型以及当前的调用链决定了应该由哪个catch的处理代码来处理该异常
被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那⼀个。根据抛出对象的类型和内容,程序的抛出异常部分告知异常处理部分到底发生了什么错误
类型不匹配:
当throw执行时,在throw后面的语句将不再被执行。程序的执行顺序从throw位置跳到与之匹配的catch模块
使用catch(…)可以用来捕获任意类型的异常,但缺点就是不知道是什么错误
抛出异常对象后,会生成⼀个异常对象的拷贝,因为抛出的异常对象可能是⼀个临时对象,所以会生成⼀个拷贝对象,这个拷贝的对象会在catch子句后销毁。
🚀四、异常的重新抛出
有时catch到一个异常对象时,需要对错误进行分类,其中某种异常错误需要进行特殊的处理,其他错误则重新抛出异常给外层调用链处理
下面就来模拟实现当聊天发消息时网络不好时需要多次尝试才能发出去或其他情况的场景:
class Exception
{
public:Exception(const string& errmsg, int id):_errmsg(errmsg), _id(id){}virtual string what() const//写成虚函数可以重写{return _errmsg;}int getid() const//用于获取异常编号{return _id;}protected:string _errmsg;//具体的错误信息int _id;//异常编号
};//关于服务的异常
class HttpException : public Exception
{
public:HttpException(const string& errmsg, int id, const string type):Exception(errmsg, id), _type(type){}virtual string what() const{string str = "HttpException: ";str += _type;str += ":";str += _errmsg;return str;}private:const string _type;
};void _SeedMsg(const string& s)
{if (rand() % 2 == 0){throw HttpException("网络不稳定,发送失败", 102, "put");}else if (rand() % 7 == 0){throw HttpException("你已经不是对象的好友,发送失败", 103, "put");}else{cout << "发送成功" << endl;}
}void SendMsg(const string& s)
{// 发送消息失败,则再重试3次 for (size_t i = 0; i < 4; i++){try{_SeedMsg(s);break;}catch (const Exception& e){// 捕获异常,if中是102号错误,⽹络不稳定,则重新发送 // 捕获异常,else中不是102号错误,则将异常重新抛出 if (e.getid() == 102){// 重试三次以后否失败了,则说明⽹络太差了,重新抛出异常 if (i == 3)throw;cout << "开始第" << i + 1 << "重试" << endl;}else{throw;}}}
}int main()
{srand(time(0));string str;while (cin >> str){try{SendMsg(str);}catch (const Exception& e){cout << e.what() << endl << endl;}catch (...){cout << "Unkown Exception" << endl;}}return 0;
}
🎉五、异常的匹配原则
1.⼀般情况下抛出对象和catch是类型完全匹配的,如果有多个类型匹配的,就选择离他位置更近的那个
2.在实际中,抛出的类型和捕获的类型不一定相匹配,如可以抛出派生类对象用基类捕获,这个在实际中非常常用
3.如果到main函数,异常仍旧没有被匹配就会终止程序,在不是发生严重错误的情况下,我们一般是不期望程序终止的,所以⼀般main函数中最后都会使用catch(…),它可以捕获任意类型的异常
🚘六、异常的安全问题
异常抛出后,后面的代码就不再执行,如果我们前面申请了资源(内存、锁等),后面进行释放,但是中间可能会抛异常就会导致资源没有释放,,这里就由于异常就引发了资源泄漏,产生安全性的问题。
因此在中间我们也需要捕获异常,释放资源后再重新抛出,当然在后面我们如果学习了智能指针章节讲的RAII方式解决这种问题是更好的。
同时在析构函数中也要注意,例如析构函数要释放5个资源,而当释放到第2个时抛出异常,则也需要捕获处理,否则后面的5个资源就没释放,也资源泄漏了。
例:
double Divide(int a, int b)
{// 当b == 0时抛出异常 if (b == 0){throw "Division by zero condition!";}return (double)a / (double)b;
}void Func()
{//由于我们开了内存,当发生除0错误时会抛出异常//而array中的内存没有得到释放//因此捕获后需要重新抛出去int* array = new int[10];try{int len, time;cin >> len >> time;cout << Divide(len, time) << endl;}catch (...){// 捕获异常释放内存 cout << "delete []" << array << endl;delete[] array;throw; // 异常重新抛出,捕获到什么抛出什么 }cout << "delete []" << array << endl;delete[] array;
}
🎡七、异常规范
在C++98中,函数参数列表的后面接throw(),表示函数不抛异常,函数参数列表的后面接throw(类型1,类型2…)表示可能会抛出多种类型的异常,可能会抛出的类型用逗号分割。
由于C++98中的这种方式过于复杂,实践中并不好用,因此在C++11中进行了简化,在函数参数列表后面加noexcept表示不会抛出异常,啥都不加表示可能会抛出异常。
注意:编译器并不会在编译时检查noexcept,也就是说如果⼀个函数用noexcept修饰了,但是同时又包含了throw语句或者调用的函数可能会抛出异常,编译器还是会顺利编译通过的(有些编译器可能会报个警告)。但是⼀个声明了noexcept的函数抛出了异常,程序会调用 terminate 终止程序。
例:
noexcept(expression)还可以作为⼀个运算符去检测⼀个表达式是否会抛出异常,可能会则返回false,不会就返回true
例:
🏝️总结(优缺点)
异常优点:可以清晰准确的展示出错误的各种信息
异常缺点:会导致程序的执行流乱跳,分析程序时比较困难,而且容易导致内存泄漏问题
今天的分享就到这里啦,感谢您的阅读,我们下期再见!