【C++】 浅析异常

所谓异常,顾名思义就是不正常,有问题。ios

对于人来讲有不正常的时候即生病身体不适,那么对于程序也同样,也有不正常即代码“有病”。程序员

那么,既然有病就要治疗,就要对症下药!这样才能恢复正常。并发


废了这么多话,仍是引出咱们C++的“异常”概念。ide

异常,让一个函数能够在发现本身没法处理的错误时抛出一个异常,但愿它的调用者能够直接或者间接处理这个问题。函数

而传统的异常处理方法:
学习

1.终止程序测试

2.返回一个表示错误的值(不少系统函数都是这样,例如malloc,内存不足,分配失败,返回NULL指针)spa

3.返回一个合法值,让程序处于某种非法的状态(最坑爹的东西,有些第三方库真会这样)操作系统

4.调用一个预先准备好在出现"错误"的状况下用的函数。3d


第一种状况是不容许的,无条件终止程序的库没法运用到不能当机的程序里。

第二种状况,比较经常使用,可是有时不合适,例如返回错误码是int,每一个调用 都要检查错误值,极不方便,也容易让程序规模加倍(可是要精确控制逻辑,我以为这种方式不错)。

第三种状况,很容易误导调用者,万一调用者没有去检查全局 变量errno或者经过其余方式检查错误,那是一个灾难,并且这种方式在并发的状况下不能很好工做。

至于第四种状况,本人以为比较少用,并且回调的代码不应多出现。




使用异常,就把错误和处理分开来,由库函数抛出异常,由调用者捕获这个异常,调用者就能够知道程序函数库调用出现错误了,并去处理,而是否终止程序就把握在调用者手里了。

可是,错误的处理依然是一件很困难的事情,C++的异常机制为程序员提供了一种处理错误的方式,使程序员能够更天然的方式处理错误。


假设咱们写一个程序,简单的除法程序:

int Div(int a, int b)
{
    if(b == 0)
       exit(1);// 如果return 0;呢?(不可取,返回0万一是10/100呢)
    return a/b;
}

int main()
{
    int a = 10;
    int b = 2;  // 若 b = 0 呢 ?
    cout<<Div(a,b)<<endl;
    return 0;
}

这样的程序,乍一看确实是没问题,可是程序在执行中当除数为0时终止了,终止意味着程序将不会继续往下执行,这就是所谓的异常。可是这样直接终止是否是有点简单粗暴呢?? 这通常不是咱们想要的结果。

C++异常中的三把斧头:try,throw,catch

①. 测试某段程序会不会发生异常

②. 如有异常发生,则经过throw抛出该异常(注:抛出的是该变量的类型)

③. 捕获相应的异常,即匹配类型的异常,进行针对性的处理


对应代码:

float Div(int a, int b)
{
    if(b == 0)
    {
       throw b;//抛出异常      
    }
    return a/b;
}

int main()
{
    int a = 10;
    int b = 0;
    float result = 0.0f;
    try
    {
       result = Div(a,b);
    } 
    catch(int)
    {
        cout<<"Div error!,除数为0"<<endl;
    }
    cout<<"result = "<<Div(a,b)<<endl;
    
    return 0;
}

运行出结果:

wKiom1bvhbXwWdGYAAAL20W8e10811.png

咱们发现,以前没有抛出异常时,程序会崩溃(除强制结束程序外),而如今没有崩溃,而且反映出了问题所在。

实际上,程序中所包含的异常现象在自身不作处理时,会交给操做系统来处理,而操做系统管理整个机器正常运转,遇到这种异常,它会直接一刀切,结束掉程序,因此会发生崩溃。而如果程序本身写了异常处理,则异常的处理由本身处理。也就是说,异常处理机制便是操做系统下发的二级机构,这个二级机构专门针对本身程序所设定的异常进行处理。



而,程序并不是一个返回值,咱们看下面:

wKiom1bvi--y_jdoAAAgXWj4HGk286.png


左边正常返回,发生异常从右边返回,发生异常后,throw以后的代码不会再执行,直接找catch惊醒捕获。那么由异常规范

class Test
{};
float Div(int a, int b)throw(int,double,short,Test)

这就是说该函数只能抛出基本类型int,double,short,以及自定义类型Test

float Div(int a, int b)throw()

这个表明该函数不能抛出异常

float Div(int a, int b)

这个表明可能抛出任何异常



此时又有一个捕获时的类型匹配问题:

float Div(int a, int b)
{
    if(b == 0)
    {
        short x = 0;
        throw x;//抛出异常      
    }
    return a/b;
}

int main()
{
    int a = 10;
    int b = 0;
    float result = 0.0f;
    try
    {
       result = Div(a,b);
    } 
    catch(int)
    {
        cout<<"Div error!(int),除数为0"<<endl;
    }
    catch(short)
    {
        cout<<"Div error!(short),除数为0"<<endl;
    }
    
    //若是抛出的是double或者char又或者其余类型呢?难道还要一直增长catch?
    //按照下面的方式能够对其余类型进行捕获
    
    catch(...) // 捕获除上面的int和short,且只能放在最后!
    {
        cout<<"Div error!(all),除数为0"<<endl;
    }
    cout<<"result = "<<Div(a,b)<<endl;
    
    return 0;
}

这有么有很像哦咱们以前学习的switch() ;   case:  语句呢?

switch()
{
    case:
    case:
    .
    .
    default:
}

至关于说,不能匹配全部的case语句,再执行default。一样,异常中亦是如此。


总结:

    异常的抛出和捕获

  1. 异常是经过抛出对象而引起的,该对象的类型决定了应该激活哪一个处理代码。

  2. 被选中的处理代码是调用链中与该对象类型匹配且离抛出异常位置最近的那一个。

  3. 抛出异常后会释放局部存储对象,因此被抛出的对象也就还给系统了,throw表达式会初始化一个抛出特殊的异常对象副本(匿名对象),异常对象由编译管理,异常对象在传给对应的catch处理以后撤销。


    栈展开

   1. 抛出异常的时候,将暂停当前函数的执行,开始查找对应的匹配catch子句。

   2. 首先检查throw自己是否在catch块内部,若是是再查找匹配的catch语句。

   3. 若是有匹配的,则处理。没有则退出当前函数栈,继续在调用函数的栈中进行查找。

   4. 不断重复上述过程。若到达main函数的栈,依旧没有匹配的,则终止程序。

   5. 上述这个沿着调用链查找匹配的catch子句的过程称为栈展开

      找到匹配的catch子句并处理之后,会继续沿着catch子句后面继续执行。



以上,咱们对于异常处理机制的原理有所了解。

对于大型的程序代码而言,就须要对于自定义类型的异常进行处理。

下面是自定义的类型匹配:

#include <iostream>
#include <string>
using namespace std;

class Exception
{
public :
     Exception(int errId, const char * errMsg)
         : _errId(errId )
         , _errMsg(errMsg )
    {}

     void What () const
    {
          cout<<"errId:" <<_errId<< endl;
          cout<<"errMsg:" <<_errMsg<< endl;
    }
private :
     int _errId ;       // 错误码
     string _errMsg ;  // 错误消息
};

void Func1 (bool isThrow)
{
     // ...
     if (isThrow )
    {
          throw Exception (1, "抛出 Excepton对象" );
    }
     // ...

     printf("Func1(%d)\n" , isThrow);
}

void Func2 (bool isThrowString, bool isThrowInt)
{
     // ...
     if (isThrowString )
    {
          throw string ("抛出 string对象" );
    }
     // ...
     if(isThrowInt )
    {
          throw 7;
    }

     printf("Func2(%d, %d)\n" , isThrowString, isThrowInt );
}

void Func ()
{
     try
    {
          Func1(false );
          Func2(true , true);
    }
     catch(const string& errMsg)
    {
          cout<<"Catch string Object:" <<errMsg<< endl;
    }
     catch(int errId)
    {
          cout<<"Catch int Object:" <<errId<< endl;
    }
     catch(const Exception& e)
    {
          e.What ();
    }
     catch(...)
    {
          cout<<" 未知异常"<< endl;
    }  
    printf ("Func()\n");
}

int main()
{
    Func();
    return 0;
}

异常的从新抛出

有可能单个的catch不能彻底处理一个异常,在进行一些校订处理之后,但愿再交给更外层的调用链函数来处理,catch则能够经过从新抛出将异常传递给更上层的函数进行处理。

class Exception
{
public :
     Exception(int errId = 0, const char * errMsg = "" )
         : _errId(errId )
         , _errMsg(errMsg )
    {}

     void What () const
    {
          cout<<"errId:" <<_errId<< endl;
          cout<<"errMsg:" <<_errMsg<< endl;
    }
private :
     int _errId ;       // 错误码
     string _errMsg ;  // 错误消息
};

void Func1 ()
{
     throw string ("Throw Func1 string");
}

void Func2 ()
{
     try
    {
          Func1();
    }
     catch(string & errMsg)
    {
          cout<<errMsg <<endl;
          //Exception e (1, "Rethorw Exception");
          //throw e ;
          // throw;
          // throw errMsg;
    }
}

void Func3 ()
{
     try
    {
          Func2();
    }
     catch (Exception & e)
    {
          e.What ();
    }
}

 异常与构造函数&析构函数

  1. 构造函数完成对象的构造和初始化,须要保证不要在构造函数中抛出异常,不然可能致使对象不完整或没有彻底初始化。

  2. 析构函数主要完成资源的清理,须要保证不要在析构函数内抛出异常,不然可能致使资源泄漏(内存泄漏、句柄未关闭等)



exception类是C++定义的一个标准异常的类,一般咱们经过继承exception类定义合适的异常类。

http://www.cplusplus.com/reference/exception/exception/

本文只是简单地从异常的使用场景,介绍了基本使用方法,一些高级的异经常使用法没有罗列,还有待补充,偏文可能有纰漏,但愿你们指出

相关文章
相关标签/搜索