异常

36 阅读5分钟

在我们写代码的过程中,出现错误是非常常见的事情,如何对一些异常进行合理方式的处理是非常重要的问题,c语言有c语言的方法,当然,cpp作为oop的语言当然也有它的一套体系。

C语言处理错误的方式

c错误处理方式

  1. 终止程序:比如assert,缺点:有些问题会直接退出会让用户非常难受,比如出现网络错误,我们通常希望的是告警,而不是进程退出。

  2. 返回错误码:返回错误码的方式并不直观,比如返回一个5,并不能直接知道出了什么错误,必须要查对应的的错误。

    大部分情况c还是采用返回错误码的方式处理错误,部分情况使用终止进程处理特别严重的错误。

C++异常概念

异常是什么?其实是一种处理错误的方式,当一个函数遇到无法处理的错误时,就可以抛出异常,让函数可以直接或者间接的调用者去处理这个错误。

大概形式如下:

try  
{  
 // 保护的标识代码  
}catch( ExceptionName e1 )  
{  
 // catch 块  
}catch( ExceptionName e2 )  
{  
 // catch 块  
}catch( ExceptionName eN )  
{  
 // catch 块  
}

异常的使用

关键字

使用异常前,先认识一下异常的关键字

  • throw:抛出异常关键字,可以抛出任意的东西(字符串、整数...)
  • try:括号内的代码就是可能会出现异常的地方。
  • catch:能捕捉抛出的异常,当然要类型对应。

异常规则

异常的抛出和捕获

  • 抛出对象的类型严格匹配catch类型
  • 抛出对象匹配最近的catch
  • 抛出的是对象的拷贝
  • catch(...) 可以捕捉任意类型的异常,用来兜底
  • 可以抛派生类对象,用父类进行捕获

类型匹配例子:

void fun()
{
 int a, b;
 cin >> a >> b;
 
 // 测试扔出三种类型,匹配对应的catch
 if (b == 0)
 {
  //throw 0;
  //throw string("除0错误");
  throw 'e';
 }
 else
 {
  cout << "a/b=" << a / b << endl;
 }
}

int main()
{
 try
 {
  fun();
 }
 catch (int errid)
 {
  cout << "errid:" << errid << endl;
 }
 catch (char errch)
 {
  cout << "errch:" << errch << endl;

 }
 catch (const string& errstr)
 {
  cout << "errstr:" << errstr << endl;
 }

 return 0;
}

运行结果如下,匹配对应的catch

子类异常基类捕获

// 基类
class Exception
{
public:
 Exception(const string& errmsg, int id)
  :_errmsg(errmsg)
  , _id(id)
 {}
 virtual string what() const
 {
  return _errmsg;
 }
 int getid() const
 {
  return _id;
 }

protected:
 string _errmsg;   // 错误信息
 int _id;          // 错误码
};

class ErrorA : public Exception
{
public:
 ErrorA(const string& errmsg, int id, const string& errorA)
  :Exception(errmsg,id)
  ,_errA(errorA)
 {
  
 }
 // 重写虚函数
 virtual string what()const
 {
  string str = _errmsg + " " + _errA;
  return str;
 }
private:
 string _errA;
};


void fun()
{
 srand(time(nullptr));
 if (rand() % 5 == 0)
 {
  throw ErrorA("错误:"100"A");
 }
}
int main()
{
 while (1)
 {
  Sleep(1000);
  try
  {
   fun();
  }
  catch (const Exception& e)
  {
   cout << e.what() << endl;
  }
  catch (...)
  {
   cout << "未知错误" << endl;
  }
 }

 return 0;
}

运行结果:

在函数调用链中异常展开匹配规则

  • 现在当前栈帧中看
  • 没有去上一层栈帧
  • 如果到了main栈帧还未匹配,终止程序
  • catch处理后,会继续执行之后的语句

函数调用例子

代码大致如上方代码,但是fun函数里面调用了div函数,并在fun函数里面进行异常捕获,同样的在main函数中,也对fun进行异常的捕获

void div()
{
 int a, b;
 cin >> a >> b;
 if (b == 0)
 {
  //throw 0;
  throw string("除0错误");
 }
 else
 {
  cout << "a/b=" << a / b << endl;
 }
}

void fun()
{
 try
 {
  div();
 }
 catch (int errid)
 {
  cout << "fun中catch:" << errid << endl;
 }
}```


运行结果:

![](https://tong-1306822294.cos.ap-beijing.myqcloud.com/tong/picture/202301082359524.png)


### 异常栈帧展开

![](https://tong-1306822294.cos.ap-beijing.myqcloud.com/tong/picture/202301090003458.png)







## 异常安全

+ 最好不要在构造函数中抛出异常,可能导致构造对象不完整
+ 最好不要在析构函数中抛出异常,可能会导致资源泄露
+ C++中异常经常会导致资源泄漏的问题,比如在new和delete中抛出了异常,导致内存泄漏,在lock和unlock之间抛出了异常导致死锁


比如下面的场景:
> 如果发生异常,就不会执行delete,这样导致了资源泄漏,当然可以在Func中的catch中再增加delete,但是还有隐藏的问题,比如,array1成功,array2失败,arr2抛异常,会抛到main函数栈帧中。会导致array1得不到释放。

```cpp
void Func()
{
 // 这里可以看到如果发生除0错误抛出异常,另外下面的array没有得到释放。
 // 所以这里捕获异常后并不处理异常,异常还是交给外面处理,这里捕获了再
 // 重新抛出去。

 // 隐患,第一个成功,第二个失败
    // 可以解决,但是很麻烦,这样的问题一般是用智能指针解决
 int* array1 = new int[10];
 int* array2 = new int[10];


 int len, time;
 cin >> len >> time;

 try
 {
  cout << Division(len, time) << endl;
 }
 catch (...)
 {
  // 若第一个new成功,第二个失败,这里还要delete arr2
  cout << "delete []" << array1 << endl;
  delete[] array1;

  cout << "delete []" << array1 << endl;
  delete[] array2; 
  
  throw// 捕获什么抛出什么
 }

 cout << "delete []" << array1 << endl;
 delete[] array1;

 cout << "delete []" << array2 << endl;
 delete[] array2;
}

int main()
{
 try
 {
  Func();
 }
 catch (const char* errmsg)
 {
  cout << errmsg << endl;
 }

 return 0;
}

异常规范

  1. 异常规格说明的目的是为了让函数使用者知道该函数可能抛出的异常有哪些。 可以在函数的后面接throw(类型),列出这个函数可能抛掷的所有异常类型。
  2. 函数的后面接throw(),表示函数不抛异常。
  3. 若无异常接口声明,则此函数可以抛掷任何类型的异常
// 这里表示这个函数会抛出A/B/C/D中的某种类型的异常  
void fun() throw(A,B,C,D);

// 这里表示这个函数只会抛出bad_alloc的异常  
voidoperator new (std::size_t size) throw (std::bad_alloc);  

// 这里表示这个函数不会抛出异常  
voidoperator delete (std::size_t size, void* ptr) throw();  

// C++11 中新增的noexcept,表示不会抛异常  
thread() noexcept;  
thread (thread&& x) noexcept;

异常的优缺点

C++异常优点

  1. 异常对象定义好之后,相比错误码的方式更能清晰准确的展示出错误的各种信息,甚至可以包含调用堆栈的信息,可以更容易定位程序bug
  2. 可以直接跳出到catch捕捉的地方,而不用像错误码一样层层返回。
  3. 很多第三方库包含异常
  4. 部分函数使用异常更好处理,比如构造函数没有返回值,不方便使用错误码方式处理,方括号的重载,pos越界错误,只能通过异常或者终止程序。

C++异常缺点

  1. 执行流可能会乱跳,运行时抛出,会比较的混乱
  2. 有性能开销
  3. 容易导致内存泄漏、死锁安全问题
  4. C++标准体系定义不好,导致大家自定义各自的异常体系,十分混乱

总结

异常尽量规范使用,不要随便抛异常,遵守如下:

  • 所有异常类型都继承于一个基类
  • 函数是否抛异常,抛什么异常,都使用 fun() throw(); 的方式规范化。

异常总体来说,利大于弊。