1. 程式人生 > >C++異常處理

C++異常處理

釋放 文件 des 每一個 ons 調用 函數調用 析構函數 捕獲異常

本文通過C語言異常處理的方式,引出C++異常處理的概念和做法,進而深入分析C++異常處理機制,和異常處理的特殊處理以及異常規格說明等內容。

1.C語言異常處理

1.1、異常的概念

  • 異常:程序在運行過程中可能產生異常(是程序運行時可預料的執行分支),如:運行時除0的情況,需要打開的外部文件不存在的情況,數組訪問越界的情況...
  • Bug:bug是程序中的錯誤,是不可被預期運行方式,如:野指針、堆內存結束後未釋放、選擇排序無法處理長度為0的數組...

1.2、C語言的異常處理

(1)經典方法:

if(判斷是否產生異常)

{

//正常代碼邏輯

}

else

{

//異常代碼邏輯

}

#include <iostream>
#include <string>

using namespace std;

double divide(double a, double b, int* valid)
{
    const double delta = 0.000000000000001;  //判斷是否非零
    double ret = 0;

    if( !((-delta < b) && (b < delta)) )
    {
        ret = a / b;

        *valid = 1;
    }
    else
    {
        *valid = 0;
    }

    return ret;
}

int main(int argc, char *argv[])
{   
    int valid = 0;
    double r = divide(1, 0, &valid);

    if( valid )
    {
        cout << "r = " << r << endl;
    }
    else
    {
        cout << "Divided by zero..." << endl;
    }

    return 0;
}

這種做法的缺陷,寫代碼時要隨時考慮到異常分支。

(2)setjmp()和longjmp()

int setjmp(jmp_buf env) //將當前上下文保存在jmp_buf結構體中
void longjmp(jmp_buf env, int val) //從jmp_buf結構體中恢復setjmp()保存的上下文,最終從setjmp函數調用點返回,返回值為val

#include <iostream>
#include <string>
#include <csetjmp>

using namespace std;
/**缺陷:setjmp()和longjmp()的引入必然涉及到全局變量,暴力跳轉導致代碼可讀性降低***/

static jmp_buf env;     //定義全局變量

double divide(double a, double b)
{
    const double delta = 0.000000000000001;     //一般不要拿浮點數和0直接做比較
    double ret = 0;

    if( !((-delta < b) && (b < delta)) )
    {
        ret = a / b;
    }
    else
    {
        longjmp(env, 1);
    }

    return ret;
}

int main(int argc, char *argv[])
{   
    if( setjmp(env) == 0 )
    {
        double r = divide(1, 1);

        cout << "r = " << r << endl;
    }
    else
    {
        cout << "Divided by zero..." << endl;
    }

    return 0;
}

上述程序的執行流:

  • int setjmp(jmp_buf env)//將當前上下文保存在jmp_buf結構體中
  • void longjmp(jmp_buf env, int val)
  • 從jmp_buf結構體中恢復setjmp()保存的上下文,最終從setjmp函數調用點返回,返回值為val
  • main函數開始,if判斷必然是成立的,然後調用divide函數,當除數為0時,進入else從jmp_buf結構體中恢復setjmp()保存的上下文,最終從setjmp函數調用點返回,返回值為val,此時main函數中的if判斷不成立,進入else答應錯誤信息。

缺陷:setjmp()和longjmp()的引入必然涉及到全局變量,暴力跳轉導致代碼可讀性降低

2、C++中的異常處理(上)

2.1、C++內置了異常處理的語法元素try、catch、throw

--try語句處理正常的代碼邏輯

--catch語句處理異常的情況

--C++通過throw語句拋出異常信息

try語句中的異常由對應的catch語句處理

函數在運行時拋出(throw)一個異常到函數調用的地方(try語句內部),try語句就會將異常交給對應的catch語句去處理

#include <iostream>
#include <string>

using namespace std;

/**函數在運行時拋出(throw)一個異常到函數調用的地方(try語句內部),try語句就會將異常交給對應的catch語句去處理**/
double divide(double a, double b)
{
    const double delta = 0.000000000000001;
    double ret = 0;

    if( !((-delta < b) && (b < delta)) )
    {
        ret = a / b;
    }
    else
    {
        throw 0;
    }

    return ret;
}

int main(int argc, char *argv[])
{    
    try
    {
        double r = divide(1, 0);

        cout << "r = " << r << endl;
    }
    catch(...)
    {
        cout << "Divided by zero..." << endl;
    }

    return 0;
}   

2.2、C++異常處理分析

--throw拋出的異常必須被catch處理

當前函數能夠處理異常,程序繼續往下執行

當前函數無法處理異常,則函數停止執行,並返回(未被處理的異常會順著函數調用棧向上傳播,直到被處理為止,否則程序將停止執行)

2.3try語句可以拋出任何類型的異常,catch語句可以定義具體的異常類型

--不同的異常由不同的catch語句負責處理

--catch(...)用於處理所有類型的異常(只能被放在最後面)

註意:任何異常都只能被捕獲(catch)一次,異常拋出後,捕獲時至上而下將嚴格的匹配每一個catch語句處理的類型,不進行任何類型的轉換

#include <iostream>
#include <string>

using namespace std;

/**異常拋出後,至上而下將嚴格的匹配每一個catch語句處理的類型,不進行任何類型的轉換**/
void Demo1()
{
    try
    {   
        throw ‘c‘;
    }
    catch(char c)
    {
        cout << "catch(char c)" << endl;
    }
    catch(short c)
    {
        cout << "catch(short c)" << endl;
    }
    catch(double c)
    {
        cout << "catch(double c)" << endl;
    }
    catch(...)
    {
        cout << "catch(...)" << endl;
    }
}

void Demo2()
{
    throw string("D.T.Software");
}

int main(int argc, char *argv[])
{    
    Demo1();

    try
    {
        Demo2();
    }
    catch(char* s)
    {
        cout << "catch(char *s)" << endl;
    }
    catch(const char* cs)
    {
        cout << "catch(const char *cs)" << endl;
    }
    catch(string ss)
    {
        cout << "catch(string ss)" << endl;
    }

    return 0;
}

3、C++中的異常處理(下)

3.1、catch重新解釋

(1)catch中捕獲的異常可以被重新解釋拋出,catch拋出的異常需要外層的try...catch...捕獲

為什麽要重新拋出異常?

實際工程中我們可以對第三方庫中拋出的異常進行捕獲、重新解釋(統一異常類型,方便代碼問題定位),然後再拋出

#include <iostream>
#include <string>

using namespace std;

void Demo()
{
    try
    {
        try
        {
            throw ‘c‘;
        }
        catch(int i)
        {
            cout << "Inner: catch(int i)" << endl;
            throw i;
        }
        catch(...)
        {
            cout << "Inner: catch(...)" << endl;
            throw;
        }
    }
    catch(...)
    {
        cout << "Outer: catch(...)" << endl;
    }
}

/*
    假設: 當前的函數是第三方庫中的函數,因此,我們無法修改源代碼

    函數名: void func(int i)
    拋出異常的類型: int
                        -1 ==》 參數異常
                        -2 ==》 運行異常
                        -3 ==》 超時異常
*/
void func(int i)
{
    if( i < 0 )
    {
        throw -1;
    }

    if( i > 100 )
    {
        throw -2;
    }

    if( i == 11 )
    {
        throw -3;
    }

    cout << "Run func..." << endl;
}

void MyFunc(int i)  //調用第三方庫函數,捕獲並重新解釋異常,然後拋出
{
    try
    {
        func(i);
    }
    catch(int i)
    {
        switch(i)
        {
            case -1:
                throw "Invalid Parameter";      //捕獲異常並重新解釋並拋出
                break;
            case -2:
                throw "Runtime Exception";
                break;
            case -3:
                throw "Timeout Exception";
                break;
        }
    }
}

int main(int argc, char *argv[])
{
    Demo();

    try
    {
        MyFunc(11);
    }
    catch(const char* cs)
    {
        cout << "Exception Info: " << cs << endl;
    }

    return 0;
}

註意:

異常的類型可以是自定義類類型,對於類類型異常的匹配依舊是至上而下、嚴格匹配
賦值兼容原則在異常匹配中依然適用,一般而言
匹配子類異常的catch放在上部
匹配父類異常的catch放在下部

#include <iostream>
#include <string>

using namespace std;

class Base
{
};
//異常的類型可以是自定義類類型
class Exception : public Base
{
    int m_id;
    string m_desc;
public:
    Exception(int id, string desc)
    {
        m_id = id;
        m_desc = desc;
    }

    int id() const
    {
        return m_id;
    }

    string description() const
    {
        return m_desc;
    }
};

/*
    假設: 當前的函數式第三方庫中的函數,因此,我們無法修改源代碼

    函數名: void func(int i)
    拋出異常的類型: int
                        -1 ==》 參數異常
                        -2 ==》 運行異常
                        -3 ==》 超時異常
*/
void func(int i)
{
    if( i < 0 )
    {
        throw -1;
    }

    if( i > 100 )
    {
        throw -2;
    }

    if( i == 11 )
    {
        throw -3;
    }

    cout << "Run func..." << endl;
}

void MyFunc(int i)
{
    try
    {
        func(i);
    }
    catch(int i)
    {
        switch(i)
        {
            case -1:
                throw Exception(-1, "Invalid Parameter");
                break;
            case -2:
                throw Exception(-2, "Runtime Exception");
                break;
            case -3:
                throw Exception(-3, "Timeout Exception");
                break;
        }
    }
}

int main(int argc, char *argv[])
{
    try
    {
        MyFunc(11);
    }

    //在定義catch語句塊時推薦使用引用作為參數(防止拷貝構造)
    // 賦值兼容原則在異常匹配中依然適用,一般而言
    catch(const Exception& e)   // 匹配子類異常的catch放在上部
    {
        cout << "Exception Info: " << endl;
        cout << "   ID: " << e.id() << endl;
        cout << "   Description: " << e.description() << endl;
    }
    catch(const Base& e)        // 匹配父類異常的catch放在下部
    {
        cout << "catch(const Base& e)" << endl;
    }

    return 0;
}

(3)工程建議:

  • 在工程中會定義一系列的異常類,每個類代表工程中可能出現的一種異常類型
  • 代碼復用時可能需要重新解釋不同的異常類
  • 在定義catch語句塊時推薦使用引用作為參數(防止拷貝構造)

3.2、標準庫異常類族

(1)C++標準庫中提供了實用異常類族,都是從exception類派生的,主要有兩個分支

--logic_error(常用於程序中可避免的邏輯錯誤)

--runtime_error(常用於程序中無法避免的惡性錯誤)

標準庫中的異常:

技術分享圖片

4、函數異常規格說明

4.1:如何判斷一個函數是否會拋出異常,會拋出那些異常?

C++語法提供了用於申明函數所拋出的異常,異常做為函數聲明的 修飾符寫函數聲明的後面。

// 可能拋出任何異常

void fun(void) ;

// 只能拋出int型異常

void fun(void) throw(int);

// 不能拋出異常

void fun(void) throw();

#include <iostream>

using namespace std;

void func() throw(int)
{
    cout << "func()";
    cout << endl;

    throw ‘c‘;
}

int main()
{
    try 
    {
        func();
    } 
    catch(int) 
    {
        cout << "catch(int)";
        cout << endl;
    } 
    catch(char) 
    {
        cout << "catch(char)";
        cout << endl;
    }

    return 0;
}

異常規格說明的意義:

  • -提示函數調用這必須做好異常處理的準備
  • -提示函數的維護者不要拋出其他異常
  • -異常規格 說明是函數接口的一部分。

4.2:如果拋出的異常不在申明列表裏會發生什麽?

函數拋出的異常不在規格說明中,全局函數unexpected()會被調用

默認的unexpected()函數會調用全局的terminate()函數

可以自定義unexpected()函數

--函數類型void(*)(void)

--能夠再次拋出異常

a)在次拋出的異常符合觸發函數的異常規格函數時,程序恢復執行

b)否則,調用terminate()函數結束程序

--調用set_unexpected()函數設置自定義的異常函數,返回值為默認的unexpected函數入口地址。

#include <iostream>
#include <cstdlib>
#include <exception>

using namespace std;

void my_unexpected()
{
    cout << "void my_unexpected()" << endl;
    // exit(1);
    throw 1;
}

void func() throw(int)
{
    cout << "func()";
    cout << endl;

    throw ‘c‘;
}

int main()
{
    set_unexpected(my_unexpected);

    try 
    {
        func();
    } 
    catch(int) 
    {
        cout << "catch(int)";
        cout << endl;
    } 
    catch(char) 
    {
        cout << "catch(char)";
        cout << endl;
    }

    return 0;
}

註意不是所有C++編譯器都支持這個標準行為,其中vs就不支持(會直接處理拋出的異常,盡管其不在異常規格申明中)。

5、異常處理深度解析

5.1:main函數拋出異常

main函數中拋出異常會發生什麽?如果異常不處理會傳到哪裏?

實驗結果證明,異常不被處理會導致程序會異常結束,並打印異常語句。

那麽異常語句是哪裏打印的?

如果異常無法被處理,terminate()函數會被自動調用,該函數用於結束異常,同時在terminate()函數中會調用庫函數abort()函數終止程序(abort函數使得程序執行異常並立即退出)。

#include <iostream>

using namespace std;

class Test 
{
public:
    Test() 
    {
        cout << "Test()"; 
        cout << endl;
    }

    ~Test() 
    {
        cout << "~Test()"; 
        cout << endl;
    }
};

int main()
{
    static Test t;

    throw 1;

    return 0;
}
    C++語法支持自定義terminate()函數的實現:

    (1)定義一個無返回值無參數的函數(函數類型為void(*)()類型)

        a)不能拋出異常

        b)必須以某種方式結束當前程序(abort/exit/…)

    (2)調用set_terminate註冊自定義的terminate()函數

        a)返回值為默認的terminate函數入口地址。

#include <iostream>
#include <cstdlib>
#include <exception>

using namespace std;

void my_terminate()
{
    cout << "void my_terminate()" << endl;
    exit(1);
}

class Test 
{
public:
    Test() 
    {
        cout << "Test()"; 
        cout << endl;
    }

    ~Test() 
    {
        cout << "~Test()"; 
        cout << endl;
    }
};

int main()
{
    set_terminate(my_terminate);

    static Test t;

    throw 1;

    return 0;
}

5.2:析構函拋出異常

析構函數中拋出異常會怎麽樣?

實驗結果證明在Linux這樣比較穩定的環境中析構函數中拋出異常會調用terminate()函數。貌似沒有什麽問題,但對於某些嵌入式系統,可能導致系統的不穩定。

結論:

terminate函數是整個程序釋放資源的最後機會。

析構函數中不能拋出異常,會導致terminate函數被多次調用,造成資源重復釋放。

C++ 標準庫中terminate()函數調用的為abort函數,直接結束程序,不會再去調用析構函數,防止析構函數中還有異常扔出

#include <iostream>
#include <cstdlib>
#include <exception>

using namespace std;

void my_terminate()
{
    cout << "void my_terminate()" << endl;
    // exit(1);
    abort();    // C++ 標準庫中terminate()函數調用的為abort直接結束程序,不會再去調用析構函數,防止析構函數中還有異常扔出
}

class Test 
{
public:
    Test() 
    {
        cout << "Test()"; 
        cout << endl;
    }

    ~Test() 
    {
        cout << "~Test()"; 
        cout << endl;

        throw 2;    // terminate函數是整個程序釋放資源的最後機會
                    // 析構函數中不能拋出異常,會導致terminate函數被多次調用,造成資源重復釋放

    }
};

int main()
{
    set_terminate(my_terminate);

    static Test t;

    throw 1;

    return 0;
}

6、try ...catch的其他寫法

try…catch用於分隔正常邏輯的代碼和異常處理代碼,可以直接將函數實現分隔為兩部分。

#include <iostream>
#include <string>

using namespace std;

int func(int i, int j) throw(int, char)
{
    if( (0 < j) && (j < 10) )
    {
        return (i + j);
    }
    else
    {
        throw ‘0‘;
    }
}

void test(int i) try
{
    cout << "func(i, i) = " << func(i, i) << endl;
}
catch(int i)
{
    cout << "Exception: " << i << endl;
}
catch(...)
{
    cout << "Exception..." << endl;
}

int main(int argc, char *argv[])
{
    test(5);

    test(10);

    return 0;
}

顯然這種寫法代碼可讀性降低。但時在一些老的代碼可能會碰到這種寫法。
本文參考自唐老師課程,特此鳴謝。

C++異常處理