【C++】 淺析異常

所謂異常,顧名思義就是不正常,有問題。

對於人來說有不正常的時候即生病身體不適,那麼對於程序也一樣,也有不正常即代碼“有病”。

那麼,既然有病就要治療,就要對症下藥!這樣才能恢復正常。


廢了這麼多話,還是引出我們C++的“異常”概念。

異常,讓一個函數可以在發現自己無法處理的錯誤時拋出一個異常,希望它的調用者可以直接或者間接處理這個問題。

而傳統的異常處理方法:

1.終止程序

2.返回一個表示錯誤的值(很多系統函數都是這樣,例如malloc,內存不足,分配失敗,返回NULL指針)

3.返回一個合法值,讓程序處於某種非法的狀態(最坑爹的東西,有些第三方庫真會這樣)

4.調用一個預先準備好在出現"錯誤"的情況下用的函數。


第一種情況是不允許的,無條件終止程序的庫無法運用到不能當機的程序裏。

第二種情況,比較常用,但是有時不合適,例如返回錯誤碼是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/

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



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章