C/C++學習筆記八(斷言與異常處理)

炫目蕭蕭發表於2017-08-13

斷言

斷言是什麼?簡單而言,斷言是對某種假設條件進行檢查。
C語言中,在assert.h中,斷言被定義為巨集的形式(assert(expression)),而不是函式。
assert將通過檢查表示式的值來決定是否需要終止程式,如果表示式為真(1)則忽略斷言,程式繼續執行。如果表示式為假(0),那麼首先向錯誤流strerr列印一條錯誤資訊,然後通過abort函式終止程式的執行。

斷言用法的簡單例子:

int a,b;
 a = 1;
 b = 1 ;
 assert(b!=0);
 printf("a/b = %d\n",a/b);

通過檢視assert.h,NDEBUG巨集開啟狀態時assert巨集是可用的。
預設情況下,assert巨集只有在Debug版本才起作用,而在Release版本中將被忽略。但在許多作業系統的C程式中,Release版本中也將NDEBUG巨集依然為開啟狀態。
也便是說如果需要用到斷言時,使用者可以通過重定義自己的ASSERT。例子如下:

#ifdef DEBUG
#define ASSERT(condition) \
    do{ \
        if(condition) \
        {  \
            NULL; \
        } \
        else{ \
            assert(condition); \
        }     \
    }while(0)
#else
#define ASSERT(condition) NULL
#endif

避免使用斷言去檢查程式錯誤

在斷言的使用中,應該遵循這樣的一個規定:對來自系統內部的可靠資料使用斷言,對於外部不可靠資料不能使用斷言,而應該使用錯誤處理程式碼。
換句話而言,斷言是用來處理不應該發生的非法情況,而對於可能發生的應該使用錯誤處理程式碼。
對於使用者輸入,與外部系統進行協議互動時的情況,也不能使用斷言進行引數的判斷,這種情況屬於正常的錯誤檢查。

下面的例子說明了斷言的使用場景

char * Strdup(const char * src){
    assert(src!=NULL);

    char * result = NULL;
    size_t len = strlen(src) +1;
    result = (char *)malloc(len);

    assert(result != NULL);
    return result;
}

例子中第一個斷言assert(src!=NULL)用於判斷傳入的引數的正確性,保證引數不為NULL
第二個斷言assert(result != NULL)檢查函式返回值是否為NULL。
例子中的兩個斷言,第一個是合法的,而第二個不合法,第一個合法是因為傳入的引數必須不為NULL,斷言如果成功,則說明呼叫程式碼存在問題,這屬於非法的情況,此處屬於斷言的正確使用情況。
第二個斷言則不同,malloc對於返回NULL的情況屬於呼叫正常情況,這應該使用正常的錯誤處理邏輯,不應該使用斷言。

避免在斷言表示式中使用改變上下文的語句

在assert巨集只有在Debug版本中情況下,應該避免斷言表示式中使用改變環境的語句。

如下例子因為斷言語句的緣故,將導致不同的編譯版本產生不同的結果。

int test(int i)
{
    assert(i++);
    return i;
}

因此應該避免在斷言表示式中使用改變上下文環境的語句,也就是確保斷言僅僅作為一個檢查而存在,不應該參與正常語句的處理。

異常處理

獲取錯誤程式碼errno

error 是用於表達不同錯誤值的一個全域性變數。如果一個系統呼叫或庫函式呼叫失敗,可以通過errno的值來確定問題所在。

因errno是一個全域性變數,在呼叫不同系統呼叫或者庫函式失敗時都有可能修改它的值,因為在使用errno時,應先將其清0

    errno = 0;

    FILE *fp = fopen("test.txt", "r");
    if (fp == NULL) {

        if (errno!=0) {
            printf("error : %d \n",errno);
            printf("錯誤資訊 : %s \n",strerror(errno));
        }

    }

但errno並不是所有的庫函式都適合使用,就error而言庫函式一般分為如下幾種。

1.函式返回值無法判斷錯誤,需進一步從errno中獲取錯誤資訊

函式 返回值 errno值
fgetwc、fputwc WEOF EILSEQ
strtol、wcstol LONG_MIN或LONG_MAX ERANGE
strtoll、wcstoll LLONG_MIN或LLONG_MAX ERANGE
strtoul、wcstoul ULONG_MAX ERANGE
strtoull、wcstoull ULLONG_MAX ERANGE
strtoumax、wcstoumax UINTLLONG_MAX ERANGE
strtod、wcstod 0或者+-HUGE_VAL ERANGE
strtof、wcstof 0或者+-HUGE_VALF ERANGE
strtold、wcstold 0或者+-HUGE_VALL ERANGE
strtoimax、wcstoimax IMAX_MIN或INTMAX_MAX ERANGE

以字串轉成長整型函式strtol為例,
在64位機器下,long長度為8位元組,最大值LONG_MAX 為 0x7fffffffffffffff,當變數longStr 取超出長整型最大值的字串”0xffffffffffffffff”和剛好等於最大值的字串”0x7fffffffffffffff”時,函式的返回值都為相同的LONG_MAX。此時金聰返回值是無法判斷函式的執行的成功與否。這個時要判斷errno的值。如下例中,會列印出錯誤的資訊。

    errno =0;

    //LONG_MAX的最大值為0x7fffffffffffffff
    const char * longStr = "0xffffffffffffffff";
    long ret = strtol(longStr,NULL,16);
    if (ret == LONG_MAX) {
        if (errno!=0) {
            printf("error : %d \n",errno);
            printf("錯誤資訊 : %s \n",strerror(errno));
        }else{
            printf("等於long的最大值\n");
        }
    }

2.函式返回值可知錯誤,errno可知更詳細的錯誤

函式 返回值 errno值
ftell() -1L positive
fgetpos()、fsetpos() nonzero positive
mbrtowc()、mbsrtowcs() (size_t)(-1) EILSEQ
signal() SIG_ERR positive
wcrtomb()、wcsrtombs (size_t)(-1) EILSEQ
mbrtoc16()、mbrtoc32() (size_t)(-1) EILSEQ
c16rtomb()、cr21rtomb (size_t)(-1) EILSEQ

3.有不同標準文件的庫函式

有些函式在不同的標準下對errno有不同的定義,例如fopen中便是一個例子。C99並沒有對使用fopen是對errno做要求,但POSIX.1卻宣告瞭錯誤時返回NULL,並將錯誤碼寫入errno。

避免使用goto語句

goto語句有很多優點,例如goto語句可以非常方便的在區域性作用域中跳出多層迴圈,執行如無條件的跳轉。
但正因為goto語句可以靈活的跳轉,如果不加以限制它會破壞程式的結構化風格,使得程式碼難以理解與測試,同時不加限制的使用goto語句可能跳過變數的初始化、重要的計算等語句。

以下例子在a小於0或者a小於等於100時會使用goto跳轉到標記為Error的語句中。
注意goto只能在區域性作用域中跳轉。

void testGoto(int a)
{
    if (a>0) {
        if (a>100) {
            printf(" a = %d \n",a);
        }else{
            goto Error;
        }
    }else{
        goto Error;
    }
Error:
    printf("Test Error a = %d \n",a);
}

避免使用setjmp與longjmp

相比與goto語句只能在區域性作用域中跳轉,setjump與longjmp可以進行跨作用域跳轉,也就是跨函式跳轉。
我們知道函式呼叫都以函式棧的形式進行呼叫與退出,既然要做到跨函式跳轉,那便需要對當前的函式棧進行儲存與還原,而setjmp的作用便是儲存當前函式棧至型別jmp_buf結構體變數中,而longjmp的作用便是從此結構體中恢復,還原函式棧。
而相對於goto僅在作用域內跳轉,setjmp和longjmp則使程式碼更加的難以維護以及可讀。

小結

  1. C語言中,使用函式的返回值來標誌函式是否執行成功(預設成功返回1,失敗返回0)當使用介面時,必須對函式進行正確性的驗證,檢查它的返回值,並且對每個錯誤的返回值進行相應的處理以及提示。
  2. 同樣的道理,如果作為介面的開發方,需要對函式的各種情況反映到返回值中。
  3. 編寫程式碼是,無論使用什麼樣的錯誤處理方式,發現程式中錯誤最好的方法便是執行程式,讓資料在函式中流動,在判斷邏輯中查詢到函式出錯的地方。

相關文章