C++11 异常(上)

简介: C++11 异常

c语言处理错误的方式

c语言传统的错误处理机制是这样子的

  1. 终止程序,如assert。缺陷:用户难以接受。如发生内存错误,除0错误时就会终止程序。
  2. 返回错误码。缺陷:需要程序员自己去查找对应的错误。如系统的很多库的接口函数都是通过把错误码放到errno中,表示错误。
  3. C标准库中setjmp和longjmp组合。(不常用)

我们在实际写c语言的过程中基本都是用错误码在报错 在发生很严重的错误时使用断言报错


C++异常概念

异常是面向对象语言常用的一种处理错误的方式 当一个函数发现自己无法处理的错误时就可以抛出异常 让函数直接或间接的调用者处理这个错误

这里有三个关键字

  • throw:当程序出现问题时 可以通过throw关键字抛出一个异常
  • try:try块中放置的是可能抛出异常的代码 该代码块在执行时将进行异常错误检测 ry块后面通常跟着一个或多个catch块
  • catch:如果try块中发生错误 则可以在catch块中定义对应要执行的代码块

语法示例如下

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


异常的用法

异常的抛出和捕获

异常的抛出和捕获的匹配原则:

  1. 异常是通过抛出对象而引发的 该对象的类型决定了应该激活哪个catch的处理代码 如果抛出的异常对象没有捕获 或是没有匹配类型的捕获 那么程序会终止报错
  2. 被选中的处理代码(catch块)是调用链中与该对象类型匹配且离抛出异常位置最近的那一个
  3. 抛出异常对象后 会生成一个异常对象的拷贝 因为抛出的异常对象可能是一个临时对象 所以会生成一个拷贝对象 这个拷贝的临时对象会在被catch捕获以后销毁(类似于函数的传值返回)
  4. catch(…)可以捕获任意类型的异常 但捕获后无法知道异常错误是什么
  5. 实际异常的抛出和捕获的匹配原则有个例外 捕获和抛出的异常类型并不一定要完全匹配 可以抛出派生类对象 使用基类进行捕获 这个在实际中非常有用


在函数调用链中异常栈展开的匹配原则:

  1. 当异常被抛出后 首先检查throw本身是否在try块内部 如果在则查找匹配的catch语句 如果有匹配的则跳到catch的地方进行处理
  2. 如果当前函数栈没有匹配的catch则退出当前函数栈 继续在上一个调用函数栈中进行查找匹配的catch 找到匹配的catch子句并处理以后 会沿着catch子句后面继续执行 而不会跳回到原来抛异常的地方
  3. 如果到达main函数的栈 依旧没有找到匹配的catch 则终止程序


现在我们使用一段代码来解释上面的匹配原则

void func1()
{
  throw string("这是一个异常");
}
void func2()
{
  func1();
}
void func3()
{
  func2();
}
int main()
{
  try
  {
    func3();
  }
  catch (const string& s)
  {
    cout << "错误描述:" << s << endl;
  }
  catch (...)
  {
    cout << "未知异常" << endl;
  }
  return 0;
}

上面的代码中有四个函数 分别是main函数 func1 func2 func3

其中main函数调用了func3 func3调用了func2 func2调用了func1 再然后func1抛出了一个异常

针对这段代码我们来分析下函数调用链中的匹配问题

当func1中的异常被抛出之后

  1. 当func1中的异常被抛出之后 首先它会判断自己是否再try的内部 判断出不在之后他会返回上一层的函数栈帧
  2. 在func2中它仍然不在try的内部 继续返回上一层的函数栈帧
  3. 在func3中它仍然不在try的内部 继续返回上一层的函数栈帧
  4. 在main函数中它判断出自己在try的内部了 并且开始找符合要求的catch语句 捕获异常成功

演示效果图如下

766033dd22c64b38b4571365bc32d12c.png

代码实际运行结果如下

bf6b4252fb84497596c68796476d9539.png

大家也许注意到了 我们在最后加上了一个catch(…) 这是因为有可能出现未知类型的异常 而此时如果该异常未被捕获 则有可能导致程序崩溃

下面是示例424e77ae343c4d14a82ad07443b7509f.png

而如果加上了最后捕获未知异常的代码就不会出现这种问题了

80f2a698fd91435d8b74e933050d7566.png

异常的重新抛出

有时候单个的catch可能不能完全处理一个异常 在进行一些校正处理以后 希望再交给更外层的调用链函数来处理 比如最外层可能需要拿到异常进行日志信息的记录 这时就需要通过重新抛出将异常传递给更上层的函数进行处理


如果直接让最外层捕获异常进行处理可能会引发一些问题 比如

void func1()
{
  throw string("这是一个异常");
}
void func2()
{
  int* array = new int[10];
  func1();
  //do something...
  delete[] array;
}
int main()
{
  try
  {
    func2();
  }
  catch (const string& s)
  {
    cout << s << endl;
  }
  catch (...)
  {
    cout << "未知异常" << endl;
  }
  return 0;
}
  1. 上面这段代码中 有三个函数 分别是func1 func2 main
  2. 其中main函数调用func2 func2开辟了一块空间之后调用func1
  3. func1抛出了一个异常开始往前找匹配的catch
  4. func1 和 func2的函数栈帧里面都没有找到 于是找到了main函数中并且异常被捕获
  5. 但是再销毁func2函数栈帧的过程中我们并没有销毁func2开辟出来的内存 于是乎造成了内存泄漏


为了解决内存泄漏问题 我们可以这样子重构代码

我们再func2中捕获func1中抛出的异常 捕获后将申请的内存释放 并且重新抛出

代码如下

void func2()
{
  int* array = new int[10];
  try
  {
    func1();
    //do something...
  }
  catch (...)
  {
    delete[] array;
    throw; //将捕获到的异常再次重新抛出
  }
  delete[] array;
}


这里有两点需要注意的

  • 因为我们不确定抛出异常的是什么类型 所以我们要使用 catch (…) 捕获
  • 此外重新抛出的时候我们直接throw就好 不用指定类型


异常安全

对于异常安全问题这里有三点建议

  1. 构造函数完成对象的构造和初始化 最好不要在构造函数中抛出异常 否则可能导致对象不完整或没有完全初始化
  2. 析构函数主要完成对象资源的清理 最好不要在析构函数中抛出异常 否则可能导致资源泄露(内存泄露、句柄未关闭等)
  3. C++中异常经常会导致资源泄露的问题 比如在new和delete中抛出异常 导致内存泄露 在lock和unlock之间抛出异常导致死锁 C++经常使用RAII的方式来解决以上问题。

RAII其实就是利用对象来管理资源的一种方式)


异常规范

为了让函数使用者知道某个函数可能抛出哪些类型的异常 C++标准规定:

  1. 在函数的后面接throw(type1, type2, …) 列出这个函数可能抛掷的所有异常类型
  2. 在函数的后面接throw()或noexcept(C++11)表示该函数不抛异常。
  3. 若无异常接口声明 则此函数可以抛掷任何类型的异常(异常接口声明不是强制的)


代码示例如下

//表示func函数可能会抛出A/B/C/D类型的异常
void func() throw(A, B, C, D);
//表示这个函数只会抛出bad_alloc的异常
void* operator new(std::size_t size) throw(std::bad_alloc);
//表示这个函数不会抛出异常
void* operator new(std::size_t size, void* ptr) throw();
// C++11
中新增的noexcept,表示不会抛异常
thread() noexcept;
thread (thread&&x) noexcept;

但是由于这个规范并不是强制性的 所以说其实对于现实写代码的影响并不是那么大 也很少有人能够按照这个规范标准来写

相关文章
|
2月前
|
算法 编译器 C语言
【C++ 异常】C++ 标准库异常类及其应用
【C++ 异常】C++ 标准库异常类及其应用
31 0
|
2月前
|
C++
C++ 捕获所有异常并拿到错误原因的方法
C++ 捕获所有异常并拿到错误原因的方法
|
2月前
|
安全 算法 C++
【C++ 异常 】深入了解C++ 异常机制中的 terminate()处理 避免不必要的错误(三)
【C++ 异常 】深入了解C++ 异常机制中的 terminate()处理 避免不必要的错误
47 0
|
2月前
|
存储 安全 算法
【C/C++ 关键字 函数说明符 】C++ noexcept 关键字(指定某个函数不抛出异常)
【C/C++ 关键字 函数说明符 】C++ noexcept 关键字(指定某个函数不抛出异常)
27 0
|
4天前
|
小程序 编译器 Linux
C++ 异常原理:以一个小程序为例
作者在调查某个 bug 时涉及到 C++ 异常,借此机会以本文把 C++ 异常机制梳理清楚供大家参考。
|
4月前
|
存储 C++
c++以exception_ptr传递异常
自C++11起,C++标准库提供一个能力:将异常存储于类型为exception_ptr的对象中,稍后才在其他情境(context)中处理它们:
16 0
|
1月前
|
C++
C++异常之栈解旋
C++异常之栈解旋
|
2月前
|
算法 程序员 编译器
【C++ 异常】深入探究C++的stdexcept类库
【C++ 异常】深入探究C++的stdexcept类库
23 0
|
2月前
|
存储 安全 NoSQL
【C++ 异常 】深入了解C++ 异常机制中的 terminate()处理 避免不必要的错误(二)
【C++ 异常 】深入了解C++ 异常机制中的 terminate()处理 避免不必要的错误
43 1
|
2月前
|
安全 程序员 编译器
【C++ 异常 】深入了解C++ 异常机制中的 terminate()处理 避免不必要的错误(一)
【C++ 异常 】深入了解C++ 异常机制中的 terminate()处理 避免不必要的错误
66 1