异常
1、C++异常概念
异常是一种处理错误的方式,当一个函数发现自己无法处理的错误时就可以抛出异常,让函数的直接或间接的调用者处理这个错误,异常使问题更简单,程序的一部分负责检测问题的出现,然后解决问题的任务传递给程序的另一部分
C语言主要通过错误码的形式处理错误,错误码本质就是对错误信息进行分类编号,拿到错误码以后还要去查询错误信息,比较麻烦。异常时抛出⼀个对象,这个对象可以函数更全面的各种信息
2、异常的抛出与捕获
- throw:当程序出现问题时,我们通过抛出(throw)一个对象引发一个异常
- catch:在想处理问题的地方,通过异常处理程序捕获异常,catch是专门用来接住、处理throw抛出来的异常错误的代码块
- try:try块中的代码标识将被激活的特定异常,它后面通常跟一个或者多个catch块
3、异常的使用
3.1 栈展开
抛出异常后,程序暂停当前函数的执行,开始寻找与之匹配的catch子句
- 首先检查throw本身是否在try块内部,如果在就查找匹配的catch语句,如果有匹配的,则跳到catch的地方进行处理
- 如果当前没有匹配的try/catch子句则退出当前的函数栈,或者有try/catch子句但类型不匹配,则退出当前函数,继续在外层调用函数链中查找,上述查找的catch过程被称为栈展开
- 如果达到main函数的栈,依旧没有匹配的catch子句,程序会终止
- 如果找到匹配的catch子句处理后,catch子句代码会继续执行

如图的三个函数func1()、func2()、func3()。在func2()中调用func1(),func3()中调用func2(),main()中调用func3(),并在func1()中抛出一个异常,在main()中用catch语句捕获
double Divide(int a, int b)
{
// 当b == 0时抛出异常
if (b == 0)
{
string s("Divide by zero condition!");
throw s;
}
else
{
return ((double)a / (double)b);
}
cout << __FUNCTION__ << ":" << __LINE__ << "行执行" << endl;
return 0;
}
void Func()
{
int len, time;
cin >> len >> time;
cout << Divide(len, time) << endl;
}
int main()
{
try
{
Func();
}
catch (const string& errmsg)
{
cout << errmsg << endl;
}
catch (int errid)
{
cout << errid << endl;
}
return 0;
}
输入1 0
输出结果:
Divide by zero condition!
栈展开的过程:
一般情况下抛出对象和catch是类型完全匹配的,如果有多个类型匹配的,就选择离他位置更近的那个
在Divide函数中加入try-catch
double Divide(int a, int b)
{
try
{
// 当b == 0时抛出异常
if (b == 0) // 输入1 0
{
string s("Divide by zero condition!");
throw s; // 抛出异常
}
else
{
return ((double)a / (double)b);
}
//… fxx()
}
catch (const int& s)
{
cout << s << endl;
}
catch (const string& errmsg)// 抛出string,类型匹配,本catch块执行
{
cout << errmsg << endl;
}
// 本地catch捕获异常,栈展开终止,继续向下执行函数剩余代码
cout << __FUNCTION__ << ":" << __LINE__ << "行执行" << endl;
return 0; // 返回0给上层Func,本行执行
}
void Func()
{
int len, time;
cin >> len >> time;
cout << Divide(len, time) << endl;// 调用Divide拿到返回值0,输出0,执行
cout << __FUNCTION__ << ":" << __LINE__ << "行执行" << endl; // 无异常抛出,本行正常执行
}
int main()
{
try
{
Func();
}
// 异常被Divide本地捕获,不会进入main的catch,不执行
catch (const string& errmsg)
{
cout << errmsg << endl;
}
catch (int errid)
{
cout << errid << endl;
}
return 0;
}
输入1 0
输出结果:
Divide by zero condition!
Divide:30行执行
0
Func:42行执行
在Divide函数中已有try-catch模块,并且离异常最近,不需要main函数中的catch块
注意:throw抛出异常后try内的其余代码不会执行了,如果本函数内有匹配的catch接住异常,那么try-catch结构之外、函数剩下的代码会正常执行,如果本函数内没有匹配的catch接住异常,那么剩下的所有代码都不会执行
eg:
try
{
throw s; // 直接跳出try,try内剩下代码全不跑
// 下面这些永远不执行
else分支return;
//…fxx();
}
catch(string& err)
{
cout << err; // 捕获异常,执行完这个块
}
// try-catch外面的代码不受异常影响,正常运行
cout << __FUNCTION__ << "执行" << endl;
return 0;
try
{
string s;
throw s; // 抛出string
}
catch(const int&) // 只能捕获int,匹配失败
{
}
// 下面两行【完全不会执行】,直接退出函数向上抛异常
cout << __FUNCTION__ << "执行" << endl;
return 0;
3.2 查找匹配的处理代码
#include<thread>
/*一般大型项目程序才会使用异常,下面我们模拟设计一个服务的几个模块
每个模块的继承都是Exception的派生类,每个模块可以添加自己的数据
最后捕获时,我们捕获基类就可以*/
// 父类 Exception(所有异常的通用模板)
class Exception
{
public:
// 构造函数:接收错误描述、错误编号
Exception(const string& errmsg, int id)
:_errmsg(errmsg)
, _id(id)
{ }
// virtual虚函数:打印异常信息,子类会重写这个函数
virtual string what() const
{
return _errmsg;
}
// 获取错误编号
int getid() const
{
return _id;
}
protected:
string _errmsg;
int _id;
};
// 构造:除了父类要的errmsg、id,额外接收出错SQL语句
class SqlException : public Exception
{
public:
SqlException(const string& errmsg, int id, const string& sql)
:Exception(errmsg, id)
, _sql(sql)
{ }
// 重写what(),拼接专属报错格式
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 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)
{
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) // 无限循环,持续模拟服务运行
{
this_thread::sleep_for(chrono::seconds(1));
try
{
HttpServer(); // 所有业务逻辑入口
}
catch (const Exception& e) // 这里捕获基类,基类对象和派生类对象都可以被捕获
{
cout << e.what() << endl; // 多态
}
catch (...) // 兜底捕获任意其他异常
{
cout << "未知异常" << endl;
}
// …
}
return 0;
}
上面的代码中父类:Exception是通用异常模板,所有模块报错都继承它,三个业务派生异常:SqlException:数据库 SQL 报错、CacheException:缓存模块报错、HttpException:网络HTTP请求报错,额外存请求方式
允许从派生类向基类类型的转换:抛出子类的对象,catch用父类的引用/指针可以直接匹配
4、异常重新抛出
有可能单个的catch不能完全处理一个异常,其中的某种异常错误需要进行特殊的处理,其他错误则重新抛出异常给外层调用链处理,catch则可以通过重新抛出将异常传递给更上层的函数进行处理
// 下面程序模拟展示了聊天时发送消息,发送失败补货异常,但是可能在
// 电梯地下室等场景手机信号不好,则需要多次尝试,如果多次尝试都发
// 送不出去,则就需要捕获异常再重新抛出,其次如果不是网络差导致的
// 错误,捕获后也要重新抛出。
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 = "xxxxxxxx";
while (cin>>str)
{
//this_thread::sleep_for(chrono::seconds(1));
try
{
SendMsg(str);
}
catch (const Exception& e)
{
cout << e.what() << endl << endl;
}
catch (...)
{
cout << "Unkown Exception" << endl;
}
}
return 0;
}
在上面的代码中,发送消息如果失败会重新发送三次消息,如果三次发送都失败那么捕捉异常
4、异常安全
构造函数完成对象的构造和初始化,最好不要在构造函数中抛出异常,否则可能导致对象不完整或没有完全初始化
其次析构函数中,最好不要在析构函数中抛出异常,如果抛出异常也要谨慎处理,比如析构函数要释放10个资源,释放到第5个时抛出异常,则也需要捕获处理,否则后面的5个资源就没释放,也资源泄漏了
5、异常规范
- 对于用户和编译器而言,预先知道某个程序会不会抛出异常会有很大帮助,有助于简化调用函数的代码
- C++98中函数参数列表的后面接throw(),表示函数不抛异常,函数参数列表的后面接throw(类型1,类型2…)表示可能会抛出多种类型的异常,可能会抛出的类型用逗号分割
// C++98写法:承诺本函数不抛异常
int Add(int a, int b) throw()
{
return a + b;
}
// 声明:本函数最多只抛string、int两种异常
void Test(int x) throw(std::string, int)
{
if (x == 0)
throw std::string("除零错误");
if (x < 0)
throw –1;
// 不能抛出string/int以外的类型,否则运行时出错
}
- C++11后,一个函数如果不抛异常,就在函数参数列表后面加noexcept,但是⼀个声明了noexcept的函数抛出了异常,程序会调用terminate 终止程序
- noexcept(expression)还可以作为一个运算符去检测一个表达式是否会抛出异常,可能会则返回false,不会就返回true
#include <iostream>
#include <string>
using namespace std;
// 标注noexcept:承诺无异常
void SafeFunc() noexcept
{
cout << "安全函数,不会报错" << endl;
// 下面放开注释,运行直接崩溃终止程序
// throw string("我违规抛异常了");
}
int main()
{
try
{
SafeFunc();
}
catch(...)
{
cout << "捕获异常" << endl;
}
return 0;
}
正常运行输出:安全函数,不会报错
如果取消throw注释:不会进入 catch,程序直接闪退终止


