C++异常机制
异常的基本概念
异常处理机制允许程序中独⽴开发的部分能够在运⾏时就出现的问题进⾏通信并做出相应的处理
C语⾔主要通过错误码的形式处理错误,错误码本质就是对错误信息进⾏分类编号,拿到错误码以后还要去查询错误信息,⽐较⿇烦
C++则是异常时抛出⼀个对象,这个对象可以函数更全⾯的各种信息。
异常的抛出与捕获
基本语法:程序出现问题时,我们通过抛出(throw)⼀个对象来引发⼀个异常,该对象的类型以及当前的调⽤链决定了应该由哪个catch的处理代码来处理该异常。
cpp
int main()
{
int m=1;
int n=0;
try{
if(n==0)
throw string("Divide by zero condition!");
}
catch (string&& s)
{
cout << s;
}
return o;
}
try和catch是处理异常信息的配合。try包含了可能含有异常错误的代码(0不能做除数),如果有错误那么就会throw抛出一个变量让catch接受,如果没有抛出那么会跳过catch。这里我们选择抛出一个常量字符串作为错误信息,让catch接受,catch需要设定能够抓取的错误信息,就像函数参数一样。上面的catch的参数就是 string&&,即抓取string类型的变量,跟我们抛出的s相同。
值得注意的是,可以有多个catch,因为异常的信息多种多样,我们会自定义多种信息,也就需要多种catch来抓取
throw执行后,它后面的代码不会执行,而是跳转到匹配的catch
蕴含两个含义:
1、沿着调⽤链的函数可能提早退出。
2、⼀旦程序开始执⾏异常处理程序,沿着调⽤链创建的对象都将销毁。
注意:
**对象拷贝:**抛出异常对象后,会⽣成⼀个异常对象的拷⻉,因为抛出的异常对象可能是⼀个局部对象,所以会⽣成⼀个拷⻉对象,这个拷⻉的对象会在catch⼦句后销毁。(这⾥的处理类似于函数的传值返回)
栈展开: 抛出异常时会沿着栈寻找最近的匹配要求的catch,进行跳转
栈展开
抛出异常后,程序暂停当前函数的执⾏,开始寻找与之匹配的catch⼦句
首先检查throw内部是否有try,如果有且有符合的catch,则跳转到catch
如果无,或者catch的类型不匹配,则退出当前函数,在外层调用的函数找,此过程为栈展开

注意
如果到达main函数,依旧没有找到匹配的catch⼦句,程序会调⽤标准库的 terminate 函数终⽌程序。
如果找到匹配的catch⼦句处理后,catch⼦句代码会继续执⾏。
异常匹配的处理规则
处理异常,遵循以下规则:
类型完全匹配
允许从⾮常量向常量的类型转换(权限缩⼩)
允许数组转换成指向数组元素类型的指针
函数被转换成指向函数的指针
允许从派⽣类向基类类型的转换(实用)
如果到达了main函数,异常没有被匹配就会终止程序,但我们一般是不愿程序终止的,所以会在main函数最后使用 catch(...),它可以捕获任意类型的异常,但是不知道异常错误是什么。
异常重新抛出
有时,我们要对异常进行分类处理,其中的某种异常在当前函数进行处理,其他异常抛给外层函数处理
eg:微信给他人发消息,计算机运行速度快,但仍出现转圈,其实就是出现异常了,但并没有立马报错,而是进行了多次尝试,还没有成功才会报错
cpp
// 模拟消息发送时,出现网络不稳定时重试
void _SendMsg(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
{
_SendMsg(s);
break; // 发送成功,退出循环
}
catch (const Exception& e)
{
// 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;
}
异常安全问题
异常抛出后,后面代码就不进行实现,但有可能出现前面申请了资源,中途抛异常,导致最后没有释放
其次抛异常后面的代码有析构函数,也会导致资源泄漏
cpp
double Divide(int a, int b)
{
if (b == 0)
{
throw "Division by zero condition!";
}
return (double)a / (double)b;
}
void Func()
{
// 异常可能导致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;
}
int main()
{
try
{
Func();
}
catch (const char* errmsg)
{
cout << errmsg << endl;
}
catch (const exception& e)
{
cout << e.what() << endl;
}
catch (...)
{
cout << "Unkown Exception" << endl;
}
return 0;
}
注意:
智能指针章节讲的RAII⽅式可以解决
在析构函数中,抛出异常要格外谨慎
异常规范
在C++11中,函数参数列表后⾯加noexcept表⽰不会抛出异常,啥都不加表⽰可能会抛出异常。(但编译器编译时不会检查noexcept,如果没有抛异常不影响,但是⼀个声明了noexcept的函数抛出了异常,程序会调⽤ terminate 终⽌程序)
noexcept(expression)还可以作为⼀个运算符去检测⼀个表达式是否会抛出异常,可能会则返回false,不会就返回true。
cpp
// noexcept表示不会抛出异常
size_type size() const noexcept;
iterator begin() noexcept;
const_iterator begin() const noexcept;
double Divide(int a, int b) noexcept
{
if (b == 0)
{
throw "Division by zero condition!"; // 违反noexcept声明
}
return (double)a / (double)b;
}
int main()
{
// noexcept运算符检测表达式是否会抛出异常
int i = 0;
cout << noexcept(Divide(1,2)) << endl; // false
cout << noexcept(Divide(1,0)) << endl; // false
cout << noexcept(++i) << endl; // true
return 0;
}
标准库的异常
C++标准库也定义了⼀套⾃⼰的⼀套异常继承体系库,基类是exception
cpp
#include <exception>
#include <iostream>
using namespace std;
int main()
{
try
{
// 可能抛出异常的操作
throw runtime_error("运行时错误");
}
catch (const exception& e) // 捕获所有标准异常
{
cout << "标准异常: " << e.what() << endl;
}
catch (...)
{
cout << "未知异常" << endl;
}
return 0;
}
总结:
C++的异常机制相比c的,信息更加全面,可以更好地帮助我们找出错误
异常匹配规则允许通过继承机制来进行异常处理