C++日积月累—异常处理

前言

异常是程序在执行期间产生的问题。C++ 异常是指在程序运行时发生的特殊情况,比如尝试除以零的操作。
异常提供了一种转移程序控制权的方式。C++ 异常处理涉及到三个关键字:try、catch、throw。

  • throw: 当问题出现时,程序会抛出一个异常。这是通过使用 throw 关键字来完成的。
  • catch: 在您想要处理问题的地方,通过异常处理程序捕获异常。catch 关键字用于捕获异常。
  • try: try 块中的代码标识将被激活的特定异常。它后面通常跟着一个或多个 catch 块。

try-catch

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

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

如果 try 块在不同的情境下会抛出不同的异常,这个时候可以尝试罗列多个 catch 语句,用于捕获不同类型的异常。

catch语句匹配被抛出的异常对象时,如果catch语句的参数是引用型,则该参数直接引用到异常对象上;如果catch语句的参数是传值的,则拷贝构造一个新的对象作为catch语句的参数的值。语句结束时,先析构catch的参数对象,再析构throw语句抛出的异常对象。

catch语句匹配异常对象时,规则很严格,不会做隐式类型转换。除了非const到constg、派生类到基类的转换、数组和函数类型转换对应的指针类型。

再catch语句中可以使用不带表达式的throw语句将捕获的异常重新抛出,让外层catch可以再次捕获。本层其他catch不能再次捕获。

throw;

被重新抛出的的异常对象,与当前catch形参无关,比如派生类的异常对象被catch的基类形参捕获,再次抛出时,任然是派生类的异常对象。但是如果在本层catch的形参是引用类型,是可能在catch中修改异常对象的。

throw

throw是一个C++关键字,与其后的表达式构成了throw语句,语法上类似于return语句。throw语句必须被包含在try块之中,可以是被包含在调用栈的外层函数的try中。所以throw关键字的作用就是主动抛出异常。
执行throw语句时,其表达式的运算结果作为对象被复制构造为一个新的对象,放在内存的特殊位置(既不是堆、也不是栈中,Windows上是放在“线程信息块TIB”中)。这个新的对象由匹配且最接近的try-catch捕获。
由于throw语句会进行一次副本拷贝,所以异常对象支持拷贝构造。

如果catch中的形参是引用类型,则异常对象只会执行一次拷贝构造。如果形参是非引用的,有n层非引用的catch就执行n+1次拷贝构造函数。

C++异常标准

C++ 提供了一系列标准的异常,定义在 中,我们可以在程序中使用这些标准的异常。它们是以父子类层次结构组织起来的,如下所示:


image.png

std::exception:该异常是所有标准 C++ 异常的父类。
std::bad_alloc:该异常可以通过 new 抛出。
std::bad_cast:该异常可以通过 dynamic_cast 抛出。
std::bad_exception:这在处理 C++ 程序中无法预期的异常时非常有用。
std::bad_typeid:该异常可以通过 typeid 抛出。
std::logic_error:理论上可以通过读取代码来检测到的异常。
std::domain_error:当使用了一个无效的数学域时,会抛出该异常。
std::invalid_argument:当使用了无效的参数时,会抛出该异常。
std::length_error:当创建了太长的 std::string 时,会抛出该异常。
std::out_of_range:该异常可以通过方法抛出,例如 std::vector 和 std::bitset<>::operator。
std::runtime_error:理论上不可以通过读取代码来检测到的异常。
std::overflow_error:当发生数学上溢时,会抛出该异常。
std::range_error:当尝试存储超出范围的值时,会抛出该异常。
std::underflow_error:当发生数学下溢时,会抛出该异常。

可以通过继承和重载 exception 类来定义新的异常。下面的实例演示了如何使用 std::exception 类来实现自己的异常:

#include 
#include 
#include 

struct MyException : public std::exception
{
    MyException(const char* p) : log(p)
    {
        std::cout << "constructor:" << this << std::endl;
    }
    MyException(const MyException& obj) : log(obj.log)
    {
        std::cout << "copy constructor:" << this << std::endl;
    }
    ~MyException()
    {
        std::cout << "destructor" << this << std::endl;
    }
    void show_log() const throw ()
    {
        std::cout << log << this << std::endl;
    }
    const char * what() const throw ()
    {
        return log.c_str();
    }
private:
    std::string log;
};


int fun(int m, int n)
{
    if (0 == n)
    {
        throw MyException("Division by zero condition!");
    }
    return m / n;
}

int main()
{
    try
    {
        fun(1, 0);
    }
    catch (MyException e)
    {
        e.show_log();
    }
}

结果:
image.png

其中what() 是异常类提供的一个公共方法,它已被所有子异常类重载。这将返回异常产生的原因。

noexcept

noexcept关键字告诉编译器,函数不会发生异常,这有利于编译器对程序做更多优化。如果运行时,noexcept函数向外抛出了异常(函数内部捕获异常并完成处理不算),程序就调用std::terminate()函数,该函数内部会调用std::abort()终止程序。
建议使用noexcept的场景:

  • 移动构造函数(move constructor)
  • 移动分配函数(move assignment),就是使用move的赋值运算符重载
  • 析构函数(destructor),析构函数默认会设置noexcept
  • 叶子函数(Leaf Function)。叶子函数是指在函数内部不分配栈空间,也不调用其它函数,也不存储非易失性寄存器,也不处理异常。

前面例子的fun加上noexcept,则再执行时会直接中断,不会抛出异常

int fun(int m, int n) noexcept
{
    if (0 == n)
    {
        throw MyException("Division by zero condition!");
    }
    return m / n;
}

还可以使用有条件的noexcept,比如把上面的noexcept改成noexcept(false),就会正常抛出异常

构造函数初始化列表的异常机制

构造函数没有返回值,所以应该用异常来报告发生的问题。构造函数抛出异常就意味着该构造函数没有执行完,所以其对应的析构函数不会被自动调用,因此构造函数应该先析构所有已初始化的基对象、成员对象,再抛出异常。

myClass::myClass(type1 pa1) 
    try:  _myClass_val (初始化值)  
{  
  /*构造函数的函数体 */
}  
  catch ( exception& err )  
{ 
  /* 构造函数的异常处理部分 */
};

析构函数被期望不向函数外抛出异常。析构函数抛出异常时,将直接调用terminator()系统函数终止程序。如果一个析构函数内部抛出了异常,就应该在该析构函数内部捕获、处理了该异常,不能让异常被抛出析构函数之外。

后记

处理各种可能发生的异常,除了抛出异常,我们还可以用返回err_code的方法来处理。
选择返回error_code还是抛异常,可以参考boost asio的设计,一个函数提供两个接口,返回error_code的和抛异常的。返回error_code的适合即时解决问题,抛异常的适合自己不处理交由别人解决。逻辑复杂的可以用状态机管理,比如boost meta state machine。

使用异常和不使用异常比,二进制文件大小会有约百分之十到二十的上升,移动平台对包体大小很敏感,所以移动平台慎用!!!

你可能感兴趣的:(C++日积月累—异常处理)