C++11异常的使用方法和优缺点介绍

异常

C98的错误处理机制:1、终止程序,如assert,缺陷:程序直接退出。如发生内存错误,除0错误时就会终止程序;2、返回错误码,缺陷:需要程序员自己去查找对应的错误。如系统的很多库的接口函数都是通过把错误码放到errno中,表示错误。

在实际使用中C语言基本都是使用返回错误码的方式处理错误,部分情况下使用终止程序处理非常严重的错误。

C++11引入了异常概念。

概念

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

关键字throw、catch、try

throw: 当问题出现时,程序会抛出一个异常。这是通过使用 throw 关键字来完成的。throw后要跟的是一个对象

catch: 在您想要处理问题的地方,通过异常处理程序捕获异常。catch 关键字用于捕获异常,可以有多个catch进行捕获。没有发生异常就会直接跳过catch最后要有个catch(...)。【像是switch case语句里面的default】

try: try 块中的代码标识将被激活的特定异常,它后面通常跟着一个或多个 catch 块。

如果有一个块抛出一个异常,捕获异常的方法会使用 try 和 catch 关键字。try 块中放置可能抛出异常的代码,try 块中的代码被称为保护代码。

格式

try
{
 // 保护的标识代码
}
catch( ExceptionName e1 )
{
 // catch 块
}
catch( ExceptionName e2 )
{
 // catch 块
}
catch(...)
{
 //实际中我们最后都要加一个catch(...)捕获任意类型的异常,否则当有异常没捕获,程序就会直接终止。
}

捕获到什么就抛出什么

try{}
catch(...)
{
	throw;
}

异常的使用

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

要注意!!类型匹配:throw的对象类型和catch接收的对象类型要一致!就近原则:抛出的异常由1个离得最近的catch处理!第一个:抛出多个异常也只会抛出第1个异常!

  1. 异常是通过抛出对象而引发的,该对象的类型决定了应该激活哪个catch的处理代码,同一个try-catch语句里,多个catch不可以接收同一个对象类型
  2. 若抛出的异常没有被catch,进程直接终止;
  3. 被选中的catch处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那一个,即一次抛出多个异常也只有1个catch来处理
  4. 捕获的是对象的拷贝,出了catch就销毁!抛出异常对象后,会生成一个异常对象的拷贝,因为抛出的异常对象可能是一个临时对象,所以会生成一个拷贝对象,这个拷贝的临时对象会在被catch以后销毁;(这里的处理类似于函数的传值返回)
  5. catch(...)可以捕获任意类型的异常,但是我们不知道异常错误是什么;
  6. 实际中抛出和捕获的匹配原则有个例外,并不都是类型完全匹配,可以抛出的派生类对象,使用基类捕获,这个在实际中非常实用,后续详细讲解。

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

首先检查throw本身是否在try块内部,在的话就查找匹配的catch语句->如果有匹配的则调到catch的地方进行处理;没有匹配的catch则退出当前函数栈,继续在调用函数的栈中进行查找匹配的catch;如果到达main函数的栈,依旧没有匹配的,则终止程序。

上述这个沿着调用链查找匹配的catch子句的过程称为栈展开。所以实际中我们最后都要加一个catch(...)捕获任意类型的异常,否则当有异常没捕获,程序就会直接终止,找到匹配的catch子句并处理以后,会继续沿着catch子句后面继续执行。

异常引发的安全问题

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

异常的重新抛出

有可能单个的catch不能完全处理一个异常,在进行一些校正处理以后,希望再交给更外层的调用链函数来处理,catch则可以通过重新抛出将异常传递给更上层的函数进行处理。也可以利用重新抛出来解决异常引发的安全问题。

C98异常规范

异常规格说明的目的是为了让函数使用者知道该函数可能抛出的异常有哪些,C++函数后面加关键字throw(something)限制,是对这个函数的异常安全性作出限制。(注:c++11使用noexcept)

  1. 可以在函数的后面接throw(类型),列出这个函数可能抛掷的所有异常类型。

    void fun() throw(exceptionType) 表示fun只能抛出exceptionType类型的异常;

    void fun() throw(A,B,C,D);表示这个函数会抛出A/B/C/D中的某种类型的异常

  2. 函数的后面接throw(),表示函数不抛异常。

    void fun() throw() 表示fun不允许抛出任何异常。

  3. 若无异常接口声明,则此函数可以抛掷任何类型的异常。

    void fun() throw(...) 表示fun可以抛出任何形式的异常。

C11–noexcept关键字

用noexcept修饰的函数不会抛出异常,在c++11中,如果noexcept修饰的函数抛出了异常,编译器可以选择直接调用std::terminate()来终止程序的运行,这比基于异常机制的throw()在效率上会高出一些。使用noexcept可有效阻止异常的传播与扩散。

STL的异常体系

  1. std::exception 该异常是所有标准 C++ 异常的父类。
  2. std::bad_alloc 该异常可以通过 new 抛出。内存申请失败就会抛异常。void* operator new (std::size_t size) throw (std::bad_alloc);
  3. std::bad_cast 该异常可以通过 dynamic_cast 抛出。
  4. std::bad_exception 这在处理 C++ 程序中无法预期的异常时非常有用。
  5. std::bad_typeid 该异常可以通过 typeid 抛出。
  6. std::logic_error 理论上可以通过读取代码来检测到的异常。
  7. std::domain_error 当使用了一个无效的数学域时,会抛出该异常。
  8. std::invalid_argument 当使用了无效的参数时,会抛出该异常。
  9. std::length_error 当创建了太长的 std::string 时,会抛出该异常。
  10. std::out_of_range 该异常可以通过方法抛出,例如 std::vector 的at访问超出有效数据范围和 std::bitset<>::operatoroperator[]使用assert。
  11. std::runtime_error 理论上不可以通过读取代码来检测到的异常。
  12. std::overflow_error 当发生数学上溢时,会抛出该异常。
  13. std::range_error 当尝试存储超出范围的值时,会抛出该异常。
  14. std::underflow_error 当发生数学下溢时,会抛出该异常。

通过继承和重载 exception 类来定义新的异常,what() 是异常类提供的一个公共方法,它已被所有子异常类重载。这将返回异常产生的原因。

异常的优缺点

优点:相比错误码的方式可以清晰准确的展示出错误的各种信息,甚至可以包含堆栈调用的信息,这样可以帮助更好的定位程序的bug;boost、gtest、gmock等等常用的第三方库都在使用异常;异常适用于如错误码若调用层数太深就比较麻烦,assert只在debug模式下有效的其他报错有缺陷的情况。

缺点:异常会导致程序的执行流乱跳,非常混乱;容易导致内存泄漏、死锁等异常安全问题,要使用RAII来处理资源的管理问题;

猜你喜欢

转载自blog.csdn.net/m0_61780496/article/details/129853378