异常是指存在于程序运行时的异常行为,这些行为超出了函数正常功能的范围,当程序的某部分检测到一个无法处理的问题时,就需要用到异常处理。
目录
1. C语言中传统的处理错误方式
终止程序:如assert,当发生错误时,直接终止程序,这样的作法不友好。
返回错误码:如果函数体里发生错误时,将错误码返回给coder,需要去查找对应的错误,系统的库函数接口就是通过把错误码放到errno中,表示错误。
Windows
下,使用perror
打印全部错误:
for (int i = 0; i < 43; ++i)
{
cout << i << " : ";
perror(strerror(i));
cout << endl;
}
大部分情况下,C语言出现错误,都是使用的是返回错误码的方式处理,部分情况下使用终止程序来处理十分严重的错误。
2. C++中处理异常的方式
如果程序中含有可能引发异常的代码,那么通常也需要有专门的代码处理问题,如:程序的问题是输入无效,则异常处理部分可能会要求用户重新输入正确的数据。
异常处理机制为程序中异常检测和异常处理这两部分的协作提供支持,C++中,异常处理包括:
- throw:异常检测部分使用
throw
来表示它遇到了无法处理的问题,此时就会抛异常; - catch:用于捕获异常,可以有多个catch同时进行捕获;
- try:try块中的代码抛出的异常通常会被一个或多个catch处理,因为catch处理异常,所以他们也被称为异常处理代码。
2.1 throw
程序的异常检测部分使用throw抛出一个异常,throw后紧跟一个表达式,该表达式的类型就是抛出的异常类型。
一般来说,直接将异常抛出,交给后面的程序处理异常,不应该将异常信息给直接输出。
int main()
{
FILE* fp = fopen("a.txt", "a");
//抛出异常的类型为string类型
if (fp == nullptr)
throw string("请检查文件是否存在");
return 0;
}
2.2 try
try关键字后,紧跟着一个块,这个块中是花括号扩起来的语句序列,跟在try块之后的是一个或多个catch子句;
catch子句包括三部分:关键字catch、括号内的对象声明(异常声明,异常类型,抛出异常的类型要和catch处理的异常类型相同),一个处理异常的代码块;
当选中某个catch子句处理异常后,执行与之对应的块,catch一旦完成,程序跳转到try语句最后一个catch之后的语句继续执行。
int main()
{
try
{
FILE* fp = fopen("a.txt", "r");// 以只读方式打开一个文件
if (fp == nullptr)
throw string("请检查文件是否存在");
}
catch (const char* msg)// char*类型异常
{
cout << msg << endl;
}
catch (const string& msg)//string类型异常
{
cout << msg << endl;
}
catch (...)//... 代表可以捕获任意类型的异常
{
cout << "出现了无法解决的异常" << endl;
}
return 0;
}
3. 异常抛出和捕获的规则
- 异常是通过抛出对象而引起的,该对象的类型决定了应该匹配哪个
catch
的处理代码; - 异常处理部分
catch
,是调用链中与该类型匹配且抛出异常位置最近的那一个; - 抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的对象可能是一个临时对象,所以会生成一个拷贝对象,该拷贝的临时对象会在
catch
结束后销毁; catch(...)
可捕获任意类型异常,代表了不知道出现异常的错误是什么,也就无法进行解决;- 在异常的抛出和捕获中,并不是类型的完全匹配,可以抛出派生类对象,使用基类捕获(很重要)。
在函数调用链中异常栈展开的匹配规则:
- 先检查
throw
是否在try
块内部,如果是则再查找匹配的catch
语句,如果有匹配则调用catch
的异常处理代码; - 若没有匹配的
catch
异常处理代码,则退出当前函数栈,继续在调用函数栈中查找匹配catch; - 如果达到main函数栈中,仍没有匹配,则终止程序,沿着调用栈查找匹配的
catch
子句的过程称为栈展开;所以一般情况下,都要在最后加一个catch(...)
捕获任意类型的异常,否则当有异常没有被捕获时,就会导致程序终止; - 找到匹配的catch子句后,会沿着catch之后的代码继续执行。
注意:
- throw可以抛出任意类型的异常,抛出的异常必须进行捕获,否则程序就会终止;
- throw抛出异常后,若是在多个函数栈中调用时,会直接跳转到有匹配的catch子句中,若没有匹配的子句时,程序终止;
catch(...)
可捕获任意类型异常。
3.1 异常重新抛出
有可能单个的异常不能完全处理一个异常,则在进行一些处理后,希望再给外层的调用链函数来处理,catch则可以通过重新抛出异常将异常传递给更上层的函数处理;
double Div(int a, int b)
{
if (b == 0)
throw string("发生了除0错误");
return a / b;
}
void func()
{
int *p = new int(10);
int b = 0;
cout << Div(*p, b);
delete p;
}
int main()
{
try
{
func();
}
catch (const string& s)
{
cout << s << endl;
}
return 0;
}
上述代码中,会出现内存泄漏,throw抛出的异常,直接跳转到main函数栈中,则会导致func中,申请的空间没有释放,造成内存泄漏,则需要对该异常进行重新捕获,并且释放该空间,避免内存泄漏。
这样修改就不会存在内存泄漏:
void func()
{
int *p = new int(10);
try
{
int b = 0;
cout << Div(*p, b);
}
catch (...)
{
delete p;
throw;
}
delete p;
}
4. 异常安全
异常安全:异常导致的安全问题。
异常中断了程序的正常流程,异常发生时,调用者请求的一部分计算可能已经完成了,另一部可能还没完成。通常情况下,略过部分程序意味着某些对象处理到一般就戛然而止了,从而导致对象处于无效或未完成的状态,或者资源没有被正常释放。
那些在异常发生期间正确执行了“清理”工作的程序被称为异常安全的代码。
注意:
- 构造函数完成对象的构造和初始化,最好不要在构造函数中抛异常,否则可能导致对象不完整或者没有完全初始化;
- 析构函数主要完成资源的清理,最好不要在析构函数中抛出异常,否则可能导致资源泄漏;
- C++中经常会导致资源泄漏的问题,如new 和 delete中抛出异常,导致内存泄漏,lock和unlock之间抛出遗产,导致死锁,C++经常使用RAII来解决上述问题;
4.1 异常规范
- 异常规则说明说明的目的是为了让函数使用者知道该函数可能抛出什么异常,在函数后面接throw,列出这个函数可能抛出的所有异常类型;
void func() throw(string, char, char*);//可抛出三种类型的异常
void* operator new(size_t) throw(bad_alloc);//只会抛bad_alloc异常
- 函数后面接throw(),表示不会抛出异常;
void func() throw();//不抛异常
void* operator new(size_t) throw();//不抛异常
- 如果没有异常接口声明,则可以抛任意类型的异常。
5. 自定义异常体系
自定义异常的体系,一般情况下,抛出派生类的异常,由基类捕获,这样在不同的派生类中,可以抛出许多不同的异常,而且具有相同的调用方式(由基类调用),避免调用混乱,方便管理。
class Exception
{
public:
Exception(const char* msg)
:_errmsg(msg)
{}
virtual string what() = 0;//纯虚函数,接口类
string _errmsg;
};
class NetException : public Exception
{
public:
NetException(const char* msg)
:Exception(msg)
{}
virtual string what()
{
return "网络错误" + _errmsg;
}
};
class SqlException : public Exception
{
public:
SqlException(const char* msg)
:Exception(msg)
{}
virtual string what()
{
return "数据库错误" + _errmsg;
}
};
那么在捕获的时候,只需要捕获基类的异常即可:
void Func()
{
if (rand() % 33 == 0)
throw SqlException("数据库启动出错");
else if(rand() % 17 == 0)
throw NetException("网络连接出错");
}
int main()
{
for (int i = 0; i < 188; ++i)
{
try
{
Func();
}
catch (Exception& e)//捕获基类 即可
{
cout << e.what() << endl;
}
}
return 0;
}
6. 标准异常
stdexcept头文件定义了几种常用的异常类:
异常 | 描述 |
---|---|
logic_error | 程序逻辑出错,一般通过读取代码来解决 |
domain_error | 逻辑错误,参数 对应的结果值不存在 |
invalid_argument | 逻辑错误:无效参数 |
length_error | 逻辑错误:创建一个超出该类型最大长度的对象 |
runtime_error | 运行时才能检测出的问题,一般无法通过读取代码来解决 |
range_error | 运行时出错:生成的结果超出了有意义的值域范围 |
overflow_error | 计算上溢 |
underflow_error | 计算下溢出 |
new头文件中,定义了bad_alloc异常类型:
异常 | 描述 |
---|---|
bad_alloc | new分配内存失败时导致的异常 |
typeinfo头文件定义了两个异常类型:
异常 | 描述 |
---|---|
bad_typeid | 对null指针的的typeid抛出异常 |
bad_cast | 动态转换失败时引发异常 |
7. 异常优缺点
优点:
- 清晰的包含错误信息;
- 如果有越界问题时,可以很方便的处理;
- 多层调用时,里层发生错误,不会层层调用,最外层可直接捕获;
- 一些第三方库也是使用异常,使用异常时可以很方便使用这些库:如boost
缺点:
- 异常会导致执行流跳转,分析程序时会有一些问题;
- C++中没有GC,异常可能会导致资源泄漏的风险;
- C++库中定义的异常体系,可用性不高,一般自己定义;
- C++可以抛任意类型的异常,则需要对异常最很好的规范管理,否则就会非常混乱,所以一般定义出继承体系下的异常规范。