目录
1.异常的概念
异常处理机制允许程序中独⽴开发的部分能够在运⾏时就出现的问题进⾏通信并做出相应的处理,异常使得我们能够将问题的检测与解决问题的过程分开,程序的⼀部分负责检测问题的出现,然后解决问题的任务传递给程序的另⼀部分,检测环节⽆须知道问题的处理模块的所有细节。
C语⾔主要通过错误码的形式处理错误,错误码本质就是对错误信息进⾏分类编号,拿到错误码以后还要去查询错误信息,⽐较⿇烦。C++异常时抛出⼀个对象,这个对象可以包含更全⾯的各种信息。
C++中要使用异常处理时,需要包含头文件exception.
2.异常的抛出和捕捉
程序出现问题时,通过抛出(throw)⼀个对象来引发⼀个异常,该对象的类型以及当前的调⽤链决定了应该由哪个catch的处理代码来处理该异常。
被选中的处理代码是调⽤链中与该对象类型匹配且离抛出异常位置最近的那⼀个。根据抛出对象的类型和内容,程序的抛出异常部分告知异常处理部分到底发⽣了什么错误。
当throw执⾏时,throw后⾯的语句将不再被执⾏。程序的执⾏从throw位置跳到与之匹配的catch模块,catch可能是同⼀函数中的⼀个局部的catch,也可能是调⽤链中另⼀个函数中的catch,控制权从throw位置转移到了catch位置。这⾥还有两个重要的含义:1、沿着调⽤链的函数可能提早退出。2、⼀旦程序开始执⾏异常处理程序,沿着调⽤链创建的对象都将销毁。
抛出异常对象后,会⽣成⼀个异常对象的拷⻉,因为抛出的异常对象可能是⼀个局部对象,所以会⽣成⼀个拷⻉对象,这个拷⻉的对象会在catch⼦句后销毁。(这⾥的处理类似于函数的传值返回)
下面这段程序就是模拟的是当除法运算中除数为0是抛出异常的情况:这里的运行逻辑是在try子句中调用Func()函数,在func()函数中输入两个浮点型的变量,通过这两个变量调用Divide()函数,当除数为0的时候使用throw语句抛出异常,由于整个调用链中只有main函数中有catch子句且throw抛出的对象类型和catch子句接收的参数类型相同,则匹配到该catch子句执行catch子句中的内容。因为在while循环中,执行完之后会再次执行Func()函数。
#include <iostream>
#include <string>
#include <exception>
using namespace std;
//只有一个try -- catch
double Divide(int a, int b)
{
//当 b == 0时抛出异常
if (b == 0)
{
string s("Divide by zero condition");
throw s; //抛出异常往上一层一层找catch
}
else
{
return ((double)a / (double)b);
}
}
void Func()
{
int len, time;
cin >> len >> time;
cout << Divide(len, time) << endl;
}
int main()
{
while (1)
{
try
{
Func();
}
catch (const string& errmsg)
{
cout << errmsg << endl;
}
}
return 0;
}

3.栈展开
抛出异常后,程序暂停当前函数的执⾏,开始寻找与之匹配的catch⼦句,⾸先检查throw本⾝是否在try块内部,如果在则查找匹配的catch语句,如果有匹配的,则跳到catch的地⽅进⾏处理。
如果当前函数中没有try/catch⼦句,或者有try/catch⼦句但是类型不匹配,则退出当前函数,继续在外层调⽤函数链中查找,上述查找的catch过程被称为栈展开。
如果到达main函数,依旧没有找到匹配的catch⼦句,程序会调⽤标准库的 terminate 函数终⽌程序。如果找到匹配的catch⼦句处理后,catch⼦句代码会继续执⾏。

这里还是用上述除法运算的例子,只不过在每一个函数中都添加了try -- catch语句。这里有两个需要注意的点:(1)一个函数模块中可以有多个catch子句,寻找catch子句时从上至下依次进行类型匹配。(2)catch子句进行类型匹配的时候不支持隐式类型转换,下列的例子中就不支持从string转换为char*对象。
下列程序中的异常是在Divide()中进行抛出,但是通过栈展开和catch语句的匹配,最终是执行main()函数中catch (const string& errmsg)子句对应的内容。
#include <iostream>
#include <string>
#include <exception>
using namespace std;
double Divide(int a, int b)
{
try
{
if (b == 0)
{
string s("Divide by zero condition!");
throw s;
}
else
{
return ((double)a / (double)b);
}
}
catch (int errid) //第一层catch捕获int,不匹配
{
cout << errid << endl;
}
return 0;
}
void Func()
{
int len, time;
cin >> len >> time;
try
{
cout << Divide(len, time) << endl;
}
catch (const char* errmsg) //第二层catch捕获const char*,不匹配
//catch (const string& errmsg)
{
cout << errmsg << endl;
}
//异常被捕获后,后面的程序正常执行
cout << __FUNCTION__ << ":" << __LINE__ << "行执行" << endl;
}
int main()
{
while (1)
{
try
{
Func();
}
catch (const string& errmsg) //第三层捕获const string,匹配
{
cout << errmsg << endl;
}
catch (...) //可以写多个catch子句,该子句捕获任意异常
{
cout << "未知异常" << endl;
}
}
return 0;
}

4.查找匹配的处理代码
⼀般情况下抛出对象和catch是类型完全匹配的,如果有多个类型匹配的,就选择离他位置更近的那个。
但是也有⼀些例外,允许从⾮常量向常量的类型转换,也就是权限缩⼩;允许数组转换成指向数组元素类型的指针,函数被转换成指向函数的指针;允许从派⽣类向基类类型的转换,这个点⾮常实⽤,实际中继承体系基本都是⽤这个⽅式设计的。
如果到main函数,异常仍旧没有被匹配就会终⽌程序,不是发⽣严重错误的情况下,我们是不期望程序终⽌的,所以⼀般main函数中最后都会使⽤catch(...),它可以捕获任意类型的异常,但是不知道异常错误是什么。
⼀般⼤型项⽬程序才会使⽤异常,下⾯我们模拟设计⼀个服务的⼏个模块每个模块的异常都是Exception的派⽣类,每个模块可以添加⾃⼰的数据最后捕获时,我们捕获基类的引用就可以了。
下列程序要完整的运行成功就需要每个模块都调用成功,这里抛出异常的情况使用随机数取模进行模拟。在每个模块都有可能抛出异常,然后统一在main()函数中通过基类的引用进行捕捉并处理。
#include <iostream>
#include <string>
#include <exception>
using namespace std;
#include<thread>
class Exception
{
public:
Exception(const string& errmsg, int id)
:_errmsg(errmsg)
,_id(id)
{}
virtual string what() const
{
return _errmsg;
}
int gitId() const
{
return _id;
}
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; //给子类增加成员_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 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 SQLMgr()
{
if (rand() % 7 == 0)
{
throw SqlException("权限不足", 100, "select * from name = '张三'");
}
else
{
cout << "SQLMgr 调用成功" << endl;
}
}
void CacheMgr()
{
if (rand() % 5 == 0)
{
throw CacheException("权限不足", 100);
}
else if (rand() % 6 == 0)
{
throw CacheException("数据不存在", 101);
}
else
{
cout << "CacheMgr 调用成功" << endl;
}
SQLMgr();
}
void HttpServer()
{
if (rand() % 3 == 0) //用随机数模拟实现错误,是3的倍数执行下列程序
{
throw HttpException("请求资源不存在", 100, "get");
}
else if (rand() % 4 == 0)
{
throw HttpException("权力不足", 101, "post");
}
else
{
cout << "HttpServer调用成功" << endl;
}
CacheMgr();
}
int main()
{
srand(time(0));
while (1)
{
//下面这句代码可以简单的理解为休眠1s
this_thread::sleep_for(chrono::seconds(1));
try
{
HttpServer();
}
catch (const Exception& e) //用父类的引用去接收,可以接收父类和子类的对象
{
cout << e.what() << endl;
}
catch (...)
{
cout << "Unkown Exception" << endl;
}
}
return 0;
}

5.异常重新抛出
有时catch到⼀个异常对象后,需要对错误进⾏分类,其中的某种异常错误需要进⾏特殊的处理,其他错误则重新抛出异常给外层调⽤链处理。捕获异常后需要重新抛出,直接 throw; 就可以把捕获的对象直接抛出。
下面程序模拟聊天时发送信息,发送失败捕捉异常,但是可能在网络不稳定的情况下进行多次重新发出,如果到一定次数都发送不出去,则再抛出异常给用户,说明网络太差导致信息发送失败。
下列函数的运行逻辑是:调用SendMsg(str)进行信息的发送,SendMsg(str)中调用_SendMsg(str),如果发送成功,则不抛异常,通过break语句结束这一轮的信息发送,如果发送失败,则抛出HttpException对象的异常,在SendMsg()中进行第一次异常捕捉,如果是网络差导致的异常,通过循环再次调用_SendMsg()进行信息发送,如果是其他原因或者是重试3次之后都没能发送成功,则在SendMsg()函数中的catch子句中重新抛出异常给到main()函数中,main()函数捕捉异常进行最终的处理。
class Exception
{
public:
Exception(const string& errmsg, int id)
:_errmsg(errmsg)
,_id(id)
{}
virtual string what() const
{
return _errmsg;
}
int gitId() 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 _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); //抛异常直接跳到catch的地方,发送成功则跳出循环,不用重试
break;
}
catch (const Exception& e)
{
//捕获异常,if中是102错误,网络不稳定,则重新发送
//捕获异常,else中不是102号错误,则将异常重新抛出到外层
if (e.gitId() == 102)
{
//重试3次以后还失败了,则说明网络太差了,重新抛出异常到外层
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;
}
catch (...)
{
cout << "Unknown Exception" << endl;
}
}
return 0;
}

6.异常的安全问题
异常抛出后,后⾯的代码就不再执⾏,前⾯申请了资源(内存、锁等),后⾯进⾏释放,但是中间可能会抛异常就会导致资源没有释放,这⾥由于异常就引发了资源泄漏,产⽣安全性的问题。为了防止这种情况,在中间我们捕获异常后,就需要在catch子句中对申请的资源进行释放之后再重新抛出异常,但这种方式也有缺陷,后⾯智能指针讲的RAII⽅式解决这种问题是更好的。
其次析构函数中,如果抛出异常也要谨慎处理,⽐如析构函数要释放10个资源,释放到第5个时抛出异常,则也需要捕获处理,否则后⾯的5个资源就没释放,也资源泄漏了。
下列程序就是在申请资源的这层栈帧中,捕捉到异常,在catch子句中对申请的资源进行释放之后再重新抛出异常。
#include <iostream>
#include <string>
#include <exception>
using namespace std;
double Divide(int a, int b)
{
// 当b == 0时抛出异常
if (b == 0)
{
throw "Division by zero condition!";
}
return (double)a / (double)b;
}
void Func()
{
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;
}
7.异常规范
对于⽤⼾和编译器⽽⾔,知道某个函数是否会抛出异常有助于简化调⽤函数的代码。
1.C++98中函数参数列表的后⾯接throw(),表⽰函数不抛异常,函数参数列表的后⾯接throw(类型1,类型2...)表⽰可能会抛出多种类型的异常,可能会抛出的类型⽤逗号分割。
2.C++98的⽅式这种⽅式过于复杂,实践中并不好⽤,C++11中进⾏了简化,函数参数列表后⾯加noexcept表⽰不会抛出异常,啥都不加表⽰可能会抛出异常。
// C++98
// 这⾥表⽰这个函数只会抛出bad_alloc的异常
void* operator new (std::size_t size) throw (std::bad_alloc);
// 这⾥表⽰这个函数不会抛出异常
void* operator delete (std::size_t size, void* ptr) throw();
// C++11
size_type size() const noexcept;
iterator begin() noexcept;
const_iterator begin() const noexcept;
3.编译器并不会在编译时检查noexcept,也就是说如果⼀个函数⽤noexcept修饰了,但是同时⼜包含了throw语句或者调⽤的函数可能会抛出异常,编译器还是会顺利编译通过的(有些编译器可能会报个警告)。但是⼀个声明了noexcept的函数抛出了异常,程序会调⽤ terminate 终⽌程序。
double Divide(int a, int b) noexcept//如果函数会抛出异常但是用noexcept修饰,当抛出异常之后会终止程序
{
if (b == 0)
{
throw "Division by zero condition!";
}
return (double)a / (double)b;
}
int main()
{
//被noexcept修饰之后的函数抛出异常会终止程序
try
{
int len, time;
cin >> len >> time;
cout << Divide(len, time) << endl;
}
catch (const char* errmsg)
{
cout << errmsg << endl;
}
catch (...)
{
cout << "Unkown Exception" << endl;
}
return 0;
}

4.noexcept(expression)还可以作为⼀个运算符去检测⼀个表达式是否会抛出异常,该表达式可能会抛出异常,则返回false,不会抛出异常就返回true。
下面第一个代码中的Divide()函数没有被noexcept修饰,第二个代码中Divide()函数被noexcept修饰。通过noexcept对被noexcept修饰的函数进行检测,会永远返回1。
double Divide(int a, int b)
{
if (b == 0)
{
throw "Division by zero condition!";
}
return (double)a / (double)b;
}
int main()
{
////将noexcept作为运算符去检测表达式会不会抛出异常
int i = 0;
cout << noexcept(Divide(1, 2)) << endl; //Divide函数可能会抛出异常,所以返回0,表示该函数不会抛异常是错误的
cout << noexcept(Divide(1, 0)) << endl;
cout << noexcept(++i) << endl; //operator++()不会抛出异常,所以返回1
return 0;
}

double Divide(int a, int b) noexcept
{
if (b == 0)
{
throw "Division by zero condition!";
}
return (double)a / (double)b;
}
int main()
{
////如果测试的函数用noexcept修饰,则用noexcept作运算符检测时会返回1,表示不会抛出异常
int i = 0;
cout << noexcept(Divide(1, 2)) << endl; //Divide函数可能会抛出异常,所以返回0,表示该函数不会抛异常是错误的
cout << noexcept(Divide(1, 0)) << endl;
cout << noexcept(++i) << endl; //operator++()不会抛出异常,所以返回1
return 0;
}

8.标准库的异常
C++标准库也定义了⼀套⾃⼰的⼀套异常继承体系库,基类是exception,所以我们⽇常写程序,在主函数捕获exception即可,要获取异常信息,调⽤what函数,what是⼀个虚函数,派⽣类可以重写。
https://legacy.cplusplus.com/reference/exception/exception/

17万+

被折叠的 条评论
为什么被折叠?



