文章目录
- 1、异常概念
- 2、实际用法
- 3、C++标准库的异常体系
- 4、重新抛出异常
- 5、优缺点
1、异常概念
C语言处理错误有assert,返回错误码来处理错误的方式,不过release模式下assert无效,错误码需要程序员自己去查看是什么错误。
C++认为应当能给到程序员一个更明确的错误,所以就出现了异常处理。当出现错误时,C++会抛一个对象,里面包含错误信息。
异常有3个关键字,throw抛出异常,try和catch捕获异常。
#include <iostream>
using namespace std;
double Division(int a, int b)
{
if (b == 0)
throw "Division by zero condition!";
else
return ((double)a / (double)b);
}
void Func()
{
int len, time;
cin >> len >> time;
cout << Division(len, time) << endl;
}
int main()
{
Func();
return 0;
}
如果没有异常,那就正常运行,如果有异常,必须被捕获,否则就只会终止程序并弹出错误窗口。捕获的位置不一定只包含有错误的代码块。捕获一般在外部捕获。
int main()//也可以写在Func函数里。不过通常写在最外面
{
try
{
Func();
}
catch (const char* str)
{
cout << str << endl;
}
return 0;
}
当出现异常后,throw会直接跳到catch处。catch只会捕获try里的代码。如果Func和main都有捕获,那么出异常会去哪里?会跳到Func函数里,它会跳到离throw最近的那个位置,然后继续执行跳到的位置的后面的代码,之后所有的捕获就不用再捕获了。
异常出现后先在异常所在的栈帧里查找catch,没有就结束这个栈帧,然后到上一层栈帧里去查找catch,还是没有结束这个栈帧,继续向上找。
捕获时参数也得匹配上类型,比如Func和main都有捕获的话,throw就会到const char*类型的那个catch处,会找匹配异常的catch,如果全都没有,那就终止程序,弹出错误窗口,因为没有捕获的,相当于只有throw,没有catch,那就直接报错。
2、实际用法
写的时候不能像上面那样粗糙,要写一个类来捕获。
class Exception
{
private:
int _errid;//错误码
string _errmsg;//错误描述
};
为什么要这么写?实际的程序中,出现异常并不是一定要立即报出来的,比如发消息,如果网络不好,发送失败,那就会重试好几次,直到到达阈值,如果还是失败,那就返回异常。
写一个伪代码
void TrySendMsg()
{
try
{
SendMsg();
}
catch(const Exception& e)
{
if (e.getErrid() == 3)//假设的一个错误码数字
{
//重试
}
else
{
//记录日志,界面展示错误信息
}
}
}
回到我们的代码就这样写
class Exception
{
public:
Exception(int errid, const string& msg)
:_errid(errid)
, _errmsg(msg)
{}
const string& GetMsg() const
{
return _errmsg;
}
int GetErrid() const
{
return _errid;
}
private:
int _errid;//错误码
string _errmsg;//错误描述
};
double Division(int a, int b)
{
if (b == 0)
{
Exception err(1, "除0错误");
throw err;
}
else
return ((double)a / (double)b);
}
void Func()
{
int len, time;
cin >> len >> time;
try
{
cout << Division(len, time) << endl;
}
catch (char str)//无法匹配异常
{
cout << str << endl;
}
cout << "Func()" << endl;
}
int main()
{
try
{
Func();
}
catch (const Exception& e)
{
//看个人需求来处理
cout << e.GetMsg() << endl;
}
return 0;
}
抛出的是局部对象err,编译器会拷贝一份,将拷贝的传给下面的e,catch完后拷贝的那份就结束生命周期了。
如果想捕获自己不知道什么类型的异常,那就这样写
try
{
Func();
}
catch (const Exception& e)
{
//看个人需求来处理
cout << e.GetMsg() << endl;
}
catch(...)
{
cout << "未知异常" << endl;
}
三个点就可以接收所有。try和catch可以套上循环。未知异常是一种底线,防止错误没被捕获到。
如果异常类型很多,不同的抛异常需求,还有很多未知异常,这如何处理?可以抛出派生类对象,用基类捕获,派生类转换到基类是天然的转换,是用切割做到的。Exception是基类,带着基础信息,其它程序员添加自己的需求来作为派生类,这里也可以用多态。
写一个模拟用继承和多态来接收类的代码
class Exception
{
public:
Exception(int errid, const string& msg)
:_errid(errid)
, _errmsg(msg)
{}
const string& what() const
{
return _errmsg;
}
int GetErrid() const
{
return _errid;
}
protected:
int _errid;//错误码
string _errmsg;//错误描述
};
//每个子类都写一个what,形成多态
class SException : public Exception
{
public:
SException(const string& msg, int errid, const string& s)
:Exception(errid, msg)
, _s(s)
{}
virtual string what() const
{
string str = "SException:";
str += _errmsg;
str += "->";
str += _s;
return str;
}
protected:
string _s;
};
class CException : public Exception
{
public:
CException(const string& errmsg, int id)
:Exception(id, errmsg)
{}
virtual string what() const
{
string str = "CException:";
str += _errmsg;
return str;
}
};
class HSException : public Exception
{
public:
HSException(const string& errmsg, int id, const string& type)
:Exception(id, errmsg)
, _type(type)
{}
virtual string what() const
{
string str = "SException:";
str += _errmsg;
str += _type;
str += _errmsg;
return str;
}
private:
const string _type;
};
void SMgr()
{
srand(time(0));
if (rand() % 7 == 0)
{
throw SException("权限不足", 100, "select * from name = '张三'");
}
cout << "调用成功" << endl;
}
void CMgr()
{
srand(time(0));
if (rand() % 5 == 0)
{
throw CException("权限不足", 100);
}
else if (rand() % 6 == 0)
{
throw CException("数据不存在", 101);
}
SMgr();
}
void HServer()
{
//模拟服务出错
srand(time(0));
if (rand() % 3 == 0)
{
throw HSException("请求资源不存在", 100, "get");
}
else if (rand() % 4 == 0)
{
throw HSException("权限不足", 101, "post");
}
CMgr();
}
int main()
{
while (1)
{
this_thread::sleep_for(chrono::seconds(1));
try
{
HServer();
}
catch (const Exception& e)
{
cout << e.what() << endl;
}
catch (...)
{
cout << "Unkown Exception" << endl;
}
}
return 0;
}
3、C++标准库的异常体系
库中的是exception。
每个异常都会找到与它最匹配的那个处理方式,直到每个异常都处理完,如果找遍了所有的也还没有处理,那就弹出错误窗口。
C++98的标准里,如果在声明一个函数后,写上throw(),就像之前写const的位置,那么这个throw()就表明这个函数不抛异常。如果throw()括号里写上异常类型,那就表明这个函数只抛这些类型的异常。实际上这个规范通常不用。
C++11中有新规则,它仍然兼容98。一个函数明确不抛异常的话,就在原先写throw()的位置写上noexcept,比如void Func() noexcept,但是还是要抛的话,那就会出错,执行不了;可能抛异常,那就什么都不写。
最好不要在构造函数抛异常,可能导致没有初始化完成。
像new/malloc/fopen/lock时不要抛异常,它们需要delete/free/fclose/unlock,因为会导致内存泄漏,文件未关闭、死锁等问题。这方面可以用智能指针来更好地解决。
4、重新抛出异常
double Division(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];
int len, time;
cin >> len >> time;
cout << Division(len, time) << endl;
cout << "delete []" << array << endl;
delete[] array;
}
int main()
{
try
{
Func();
}
catch (const char* errmsg)
{
cout << errmsg << endl;
}
return 0;
}
Func里可以看到如果发生除0错误抛出异常,下面的array就没有得到释放。所以捕获异常后应当不处理异常,异常交给外面处理,这里捕获了再重新抛出去。
void Func()
{
int* array = new int[10];
int len, time;
cin >> len >> time;
try
{
cout << Division(len, time) << endl;
}
catch (const char* errmsg)
{
cout << errmsg << endl;
}
cout << "delete []" << array << endl;
delete[] array;
}
不过我们仍然可以交给main来处理,Func那里抓到异常后,先执行后面的,然后再次throw,就是重新抛出异常。
void Func()
{
int* array = new int[10];
int len, time;
cin >> len >> time;
try
{
cout << Division(len, time) << endl;
}
catch (const char* errmsg)
{
cout << "delete []" << array << endl;
delete[] array;
throw errmsg;
}
}
还应当在main里加上捕获未知异常。如果Func里有调用多个不同的函数,那么可以写上捕获所有的catch,也就是catch(…)。不过捕获异常最好全都在一起,比如main里,方便记录。
5、优缺点
优点:
1、相比错误码,能更清晰地展示各种错误信息,附带各种需要的数据
2、错误码是层层返回的
3、第三方库的异常需要捕捉
4、部分函数使用异常更好处理,比如构造函数没有返回值,越界等问题。、
缺点:
1、异常容易乱跳,导致程序的执行流乱跳,调试时会直接跳到catch处
2、异常容易导致内存泄漏、死锁等安全问题,C++也没有垃圾回收机制
3、C++标准库的异常体系不怎么好,一般都使用自定义的异常体系
4、异常需要规范起来,否则捕获就很不容易。抛出的异常类型都继承一个基类,以及无论抛什么异常都用noexcept等来规范起来
本篇gitee
结束。