异常捕获
- 异常
- 概念
- 处理错误方式
- 异常处理
- 举例
- 栈展开
- 异常规范
- 异常继承层次
- 优缺点
异常
概念
异常时程序可能检测到的,运行时不正常的情况,如存储空间耗尽,数组越界等,可以预见可能发生在什么地方但不知道在什么时候发生的错误。
举例:
#include<vector>
using namespace std;
double Div(double a, double b)
{
return a/b;
}
int main()
{
double x,y,z;
cin>>x>>y; // 输入 1,2; 输入 2, 0 ;
z = Div(x,y);
cout<<"z: "<<z<<endl;
return 0;
}
例如上面两数相除的程序,分母为0便是错误。这就是异常。
处理错误方式
C语言中解决方法:
- assert断言终止程序,断言不成立就会调用abort函数终止程序。但是该方法只在Debug版本有效,Release版本不会处理。
- 返回错误码:很多库的接口函数都是通过把错误码放到errno中,表示错误信息。
- 日志文件:将错误信息写入日志文件,出现致命错误终止程序。
C++中异常机制:
C++提供了一些内置的语言特性产生或者抛出异常,用来通知异常发生,然后预先按程序段来捕获catch,并且对他继续处理。
举例:
class my_overflow_error { // 算术计算上溢
const char* m_what;
public:
my_overflow_error() :m_what(nullptr) {}
my_overflow_error(const char* msg) :m_what(msg) {}
~my_overflow_error() { m_what = nullptr; }
const char* what() const {
return m_what != nullptr ? m_what : "unkonwn exception";
}
};
double Div(double a, double b)
{
if (0 == b)
{
throw my_overflow_error("除0错误");
}
return a / b;
}
int main()
{
double x, y, z;
cin >> x >> y; // 输入 1,2; 输入 2, 0 ;
try
{
z = Div(x, y);
cout << "z: " << z << endl;
}
catch (my_overflow_error& e)
{
cout << e.what() << endl;
}
return 0;
}
编写步骤:
- throw表达式抛出异常。throw表达式也可以抛出任何类型的对象,如美剧,整除等。但最常用的是类对象。
- 使用try关键字,构成try语句块,该语句调用的函数能够抛出异常语句。
- 由catch字据捕获并处理异常。
异常处理
举例
template<class _Ty>
class PopOnEmpty
{
public:
PopOnEmpty() = default;
~PopOnEmpty() = default;
};
template<class _Ty>
class PushOnFull
{
private:
_Ty _value;
public:
PushOnFull(const _Ty& x) :_value(x) {}
~PushOnFull() = default;
const _Ty& Value() const { return _value; }
};
template<class _Ty>
class SeqStack
{
private:
_Ty* _data;
int _capa; // 容量
int _top; // 栈顶指针
public:
SeqStack(int sz = 10) :_capa(sz), _top(-1) {
//_data = new _Ty[_capa];
_data = (_Ty*)malloc(sizeof(_Ty)*_capa);
}
~SeqStack() {
free(_data);
//delete[]_data;
}
int GetSize() const { return _top + 1; }
bool IsEmpty() const { return GetSize() == 0; }
bool IsFull() const { return GetSize() == _capa; }
void Push(const _Ty& val)
{
if (IsFull())
{
throw PushOnFull<_Ty>(val);
}
//_data[++_top] = val;
new(&_data[++_top]) _Ty(val);
}
const _Ty Pop()
{
if (IsEmpty())
{
throw PopOnEmpty<_Ty>();
}
return std::move(_data[_top--]);
}
const _Ty top() {
if (IsEmpty()) {
throw PopOnEmpty<_Ty>();
}
return _data[top];
}
void PrintStack() const
{
for (int i = 0; i <= _top; ++i)
{
cout << _data[i] << " ";
}
cout << endl;
}
};
int main()
{
const int n = 10;
int ar[n] = { 12,23,34,45,56,67,78,89,90,100 };
int br[n] = {};
SeqStack<int> istack(8);
try
{
for (int i = 0; i < n; ++i)
{
istack.Push(ar[i]);
istack.PrintStack();
}
}
catch (PushOnFull<int>& e)
{
cout << "栈满 ... 未入栈的数据是: " << e.Value() << endl;
}
try
{
for (int i = 0; i < n; ++i)
{
br[i] = istack.Pop();
}
}
catch (PopOnEmpty<int>)
{
cout << "栈空" << endl;
for (int i = 0; i < n; ++i)
{
cout << br[i] << " ";
}
cout << endl;
}
return 0;
}
观察上面代码,重点:
- 在构造和析构栈的时候用了malloc和free而没有用new和delete来创建和释放栈,因为new在申请内存之后会直接进行创建对象,而malloc只申请内存,在初始化栈的时候只需要申请空间,不把对象存放进去。
- 在push入栈的时候,会发现用了定位new而没有直接进行赋值,这里的原因是这样的。如果是内置类型这没有太大区别,但是如果是自定义的类对象,把对象给内存,如果不存在虚函数就不存在问题,但是如果存在虚表其赋值不会把虚函数也进行赋值,只有通过定位new重新对内存进行操作才可以把对象完整的移动到栈中。
- 我们发现在Pop函数中我们都是以值类型返回而不是以对象进行返回,而且以值返回时用了move函数。原因是这样:如果以引用返回,举这样一个例子,如果栈中存在5个对象,出栈一个对象之后,但是以引用返回也就是说这个值在逻辑上不在栈中,但是物理上还在栈内,但在多线程的情况下另一个线程进行了入栈操作,那么就对我们返回的值进行了覆盖,返回的引用对象也就发生了改变。如果以值类型返回,也会出现一些问题,如果是自定义的类,出栈之后,会进行拷贝构造,而如果是拷贝构造,另一个线程进行入栈之后就会对原本的位置进行覆盖,原本内存的数据没有进行释放,导致内存泄漏。所以呢我们用了move函数,move函数移动了原本栈中的资源来移动构造了新对象,也不会出现内存泄漏。
- 然后说一下异常捕获,在try区域出现异常之后,会直接跳过try中后面的代码在catch中进行处理该异常,不会处理完返回原本位置继续执行。
栈展开
因发生异常而逐步退出复合语句和函数的过程,被称为栈展开。
在throw中捕获到异常之后会通过异常类来创建一个异常对象来通过catch来解决这个异常,如果没有解决这个异常会跳出这一层,谁来调用这一部分,在上层来找catch来解决这个问题,如果在主函数中都没找到,那么会通过操作系统来终止程序处理。
异常规范
异常规范在于提供一种方案,可以列出想要抛出的异常。
template<class _Ty>
class SeqStack
{
//...
public:
void Push(const _Ty& val) throw(PushOnFull<_Ty>)
{
if (IsFull())
{
throw PushOnFull<_Ty>(val);
}
_data[++_top] = val;
}
const _Ty& Pop() throw(PopOnEmty<_Ty>)
{
if (IsEmpty())
{
throw PopOnEmpty<_Ty>();
}
return _data[_top--];
}
};
我们会发现在函数后加上了throw(类型名),该语句意为throw中参数的类型可以捕获,其他类型不需要捕获。虽然该方法已经C++11被舍弃了,但还需要了解一些。
在C++11中新增了noexcept关键字以表示这个函数不会抛出某种异常。并且可以阻止异常的传播。
void func() throw();//展开,还会找catch
void func() noexcept;//不展开,在当前函数中直接终止,不会catch
void func() noexcept(false);//假的不抛出异常
void func() noexcept(true);//真的不抛出异常
异常继承层次
上图中Excp继承出pushonFull和stackExcp类型,而(…)可以处理所有异常,所以呢要从pushonFul类型开始catch,如果直接用(…)处理其他catch就没意义了。
优缺点
使用C++异常机制缺点:
- 增加开销,以来于处理器,操作系统,编译器等,程序性能明显下降,所以不建议使用异常。
- 削弱编码人员安全意识。
- 打乱程序正常执行流程,增加结构复杂度。
- 资源释放不彻底,可能导致内存泄漏。
- 降低代码复用率,使用了异常机制的代码不能直接给不适用异常机制的代码复用。
使用建议:
在C++语言本身抛出异常(new失败,STL),第三方库,系统库的接口抛出异常时,可以使用异常捕获机制。注意构造函数和析构函数不要抛出异常,因为对象构建一般抛出异常无法处理,释放资源时释放一半抛出异常难以处理。