【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/

本文只是簡單地從異常的使用場景,介紹了基本使用方法,一些高級的異經常使用法沒有羅列,還有待補充,偏文可能有紕漏,但願你們指出

相關文章
相關標籤/搜索