目录
异常是一种处理错误的方式。
一、前言
C语言传统的处理错误的方式:
1.终止程序。一种是由操作系统来终止程序,比如发生内存错误(越界访问),除0错误等等;另一种是用户使用assert断言函数。这种方式的缺陷就是发生错误会直接终止程序。
2.返回错误码。比如fopen等函数发生错误,系统会将错误码存放到变量errno中,程序员需要自己去查找对应的错误。
在C语言中,一般多是使用错误码的方式处理错误,而对于非常严重必须及时处理的错误会直接终止程序处理。
因此,诞生了异常是一种新的错误处理方式,不仅仅是C++,面向对象的语言都使用这种错误处理方式。
异常:当一个函数发现自己无法处理的错误时就可以抛出异常,让函数的直接或者间接的调用者处理这个函数。
相关的一些关键字:
- throw:当问题出现时,程序会抛出一个异常。
- catch:在您想要处理问题的地方,通过异常处理程序捕获异常。
- try:try块中的代码用来标识将被激活的特定异常,它的后面通常连接一个或多个catch块。
如果有一个块抛出一个异常,捕获异常的方法会使用try和catch关键字。try块中放置可能抛出异常的代码,try块中的代码被称为保护代码。
异常简单使用:
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()
{
try
{
Func();
}
catch (const char* errmsg)
{
cout << errmsg << endl;
}
cout << "---------------------" << endl;
return 0;
}
注意:
在多个函数栈帧调用中,比如这里main函数调用Func,Func调用Division,但是异常是在Division函数中抛出,而捕获异常是在main函数中做的,因此当异常被抛出后,可以直接返回到main函数中。
此外,异常捕获后,程序从捕获异常的地方继续往下运行,从这里打印了一行水平线就可以看出来。
二、异常的使用
异常的抛出和匹配原则
- 异常是通过抛出对象而引发的,该对象的类型决定了应该激活哪一个catch的处理代码。
比如Division抛出异常的类型会匹配第一个catch
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()
{
try
{
Func();
}
catch (const char* errmsg)//匹配该catch
{
cout << errmsg << endl;
}
catch (int x)
{
cout << __LINE__ <<"捕获异常:"<< x << endl;
}
cout << "---------------------" << endl;
return 0;
}
- 被选中的处理代码是函数栈帧调用链中与该对象类型匹配且离抛出异常位置最近的一个。
- 抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象,所以会生成一个拷贝对象,这个拷贝的临时对象会在被catch捕捉以后销毁。(类似函数传值返回)。
- 因为异常类型和catch类型要一一对应,因此,在实际的项目中,为了防止漏写一个捕捉catch,可以使用catch(...)捕获任意类型异常,只是该捕获不清楚异常错误是什么。
int main()
{
try
{
Func();
}
catch (const char* errmsg)
{
cout << errmsg << endl;
}
catch (int x)
{
cout << __LINE__ <<"捕获异常:"<< x << endl;
}
catch (...)
{
cout << "未知异常" << endl;
}
cout << "---------------------" << endl;
return 0;
}
在函数调用栈帧中异常匹配原则
- 首先检查throw本身是否在try块内部,如果是就直接查找匹配的catch语句。如果有匹配的,则直接调到catch的地方进行处理。
- 如果没有匹配的catch则退出当前函数栈,继续在调用该函数的栈中进行查找匹配的catch。
- 如果到达main函数所在的栈,依旧没有匹配的catch,则终止程序。
- 上述这个沿着调用栈帧链查找匹配的catch的过程称为栈展开。所以实际中,通常都要在最后加catch(...),用来捕获任意类型的异常,防止有异常没有被捕获而导致程序终止。
- 找到匹配的catch块并处理以后,会继续沿着catch块后面的语句继续执行。
服务器开发中使用的异常继承体系
这种设计其实是为了规范使用异常的代码,异常的类型众多,如果不加以规范,那么捕捉异常就变得相当困难。
- 实际中抛出和捕获异常有个例外,并不都是类型完全匹配,可以抛出派生类的对象,使用基类对象捕获。服务器开发就是基于这一点。
class Exception
{
public:
Exception(const string& errmsg, int id)
:_errmsg(errmsg)
, _id(id)
{}
virtual string what() const
{
return _errmsg;
}
protected:
string _errmsg; // 错误描述
int _id; // 错误编号
};
class HttpServerException : public Exception
{
public:
HttpServerException(const string& errmsg, int id, const string& type)
:Exception(errmsg, id)
, _type(type)
{}
virtual string what() const
{
string str = "HttpServerException:";
str += _errmsg;
str += "->";
str += _type;
return str;
}
private:
const string _type;
};
void HttpServer()
{
A aa;
srand(time(0));
if (rand() % 3 == 0)
{
throw HttpServerException("请求资源不存在", 100, "get");
}
else if (rand() % 4 == 0)
{
throw HttpServerException("权限不足", 101, "post");
}
CacheMgr();
}
int main()
{
srand(time(0));
while (1)
{
Sleep(1000);
try {
HttpServer(); // io
}
catch (const Exception& e) // 这里捕获父类对象就可以
{
// 多态
cout << e.what() << endl;
}
catch (...)
{
cout << "Unkown Exception" << endl;
}
}
return 0;
}
异常规范
C++设计者提倡使用异常规格声明,尽可能让函数使用者知道该函数可能抛出哪些异常。
// 这里表示这个函数会抛出A/B/C/D中的某种类型的异常
void fun() throw(A,B,C,D);
// 这里表示这个函数只会抛出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 中新增的noexcept,表示不会抛异常
thread() noexcept;
thread (thread&& x) noexcept;
异常的重新捕获
double Division(int a, int b) throw(const char*)
{
// 当b == 0时抛出异常
if (b == 0)
{
throw "Division by zero condition!";
}
return (double)a / (double)b;
}
void fyy() noexcept
{
int len, time;
cin >> len >> time;
cout << Division(len, time) << endl;
}
void Func()
{
// 这里可以看到如果发生除0错误抛出异常,另外下面的array没有得到释放。
// 所以这里捕获异常后并不处理异常,异常还是交给外面处理,这里捕获了再
// 重新抛出去。
int* array = new int[10];
try {
fyy();
}
catch (...)
{
// 捕获异常不是为了处理异常
// 是为了释放内存,然后异常再重新抛出
cout << "delete []" << array << endl;
delete[] array;
throw; // 捕到什么抛什么
}
cout << "delete []" << array << endl;
delete[] array;
}
通过这个例子也能发现一点,fyy函数的确不抛异常,但是该函数调用的Division函数会抛异常,因此异常的声明不能完全可靠。
异常安全
- 构造函数完成对象的构造和初始化,最好不要在构造函数中抛出异常,否则可能导致构造对象初始化不完整,析构时出现内存错误。
- 析构函数主要完成资源的清理,最好不要在析构函数中抛出异常,否则可能导致资源泄露(内存泄露、句柄未关闭)。
class A
{
public:
//不安全的写法
A()
{
cout << "A()" << endl;
_ptr1 = new int;
/*int x, y;
cin >> x >> y;
Division(x, y);*/
_ptr2 = new int;
}
~A()
{
cout << "~A()" << endl;
delete _ptr1;
/*int x, y;
cin >> x >> y;
Division(x, y);*/
delete _ptr2;
}
private:
int* _ptr1;
int* _ptr2;
};
- C++中异常会经常导致资源泄露的问题,比如在new和delete中抛出了异常,导致内存泄露。在lock和unlock之间抛出异常导致死锁,C++使用RAII来解决上述问题。
//如果不使用智能指针,要规避new和delete的异常的代码十分复杂
void fxx()
{
int* p1 = new int[10];
int* p2, *p3;
try
{
p2 = new int[20];
try {
p3 = new int[30];
}
catch (...)
{
delete[] p1;
delete[] p2;
throw;
}
}
catch (...)
{
delete[] p1;
throw;
}
delete[] p1;
delete[] p2;
delete[] p3;
}
三、STL中的异常
在、
等多个头文件中,
std::exception | 该异常是所有标准C++异常的父类 |
std::bad_alloc | 该异常可以通过new抛出 |
std::bad_cast | 该异常可以通过dynamic_cast抛出 |
std::bad_exception | 在处理C++程序中无法预期的异常时非常有用 |
std::bad_typeid | 该异常可以通过typeid抛出 |
std::logic_error | 理论上可以通过读取代码来检查到的异常 |
std::domain_error | 当使用一个无效的数域时,会抛出该异常 |
std::invalid_argument | 当使用无效的参数时,会抛出该异常 |
std::length_error | 当创建了太长的string,会抛出该异常 |
std::out_of_range | 该异常可以通过函数抛出,例如vector和bitset的operator[]() |
std::runtime_error | 理论上不可以通过读取代码来检测到的异常 |
std::overflow_error | 当发生数学上溢,会抛出该异常 |
std::range_error | 当尝试存储超出范围的值,会抛出该异常 |
std::underflow_error | 当发生数学下溢,会抛出该异常 |
四、异常的优缺点
优点
- 异常对象的设计,相比错误码的方式可以清晰准确的展示出错误的各种信息,甚至可以包含堆栈的调用信息,可以更好的帮助定位程序的bug。
-
返回错误码的问题在于,在函数层层的调用栈帧中,深层的函数返回了错误,必须得 层层返回才能拿到错误。而异常可以 直接跳转到捕捉异常的地方,同时,这是异常的优点,也是异常的缺点。
- 许多第三方库也包含异常,如boost、gtest、gmock,为了使用这些库,程序员也必须熟练掌握异常的使用。
- 部分函数使用异常更好处理,比如有些函数没有返回值,比如构造函数,不方便使用返回错误码的方式,抛异常是更优的解法。还有像operator[]这些函数,如果pos越界,只能使用抛异常或者直接终止程序。
缺点
- 异常会导致程序的执行流乱跳,并且非常混乱,并且是运行时出错抛异常就会乱跳。这会导致我们调试跟踪程序更加困难。
- C++没有垃圾内存回收机制,所有资源需要自己管理。因此,异常的出现非常容易导致内存泄露、死锁等问题,不过可以使用RAII来处理相关问题。
- 异常尽量规范使用,随便抛异常会导致捕获异常困难无比。