[C 語言] 程式設計教學:錯誤處理 (Error Handling)

【分享本文】
Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email

    前言

    即使程式本身沒有錯誤,不代表程式運行時不會發生錯誤;程式要面對許多外部錯誤,像是權限不足、檔案內容錯誤、網路無法連線等。一開始學程式設計時,我們會先忽略錯誤處理的部分,這是為了簡化範例程式碼,讓程式碼更易於學習。但我們在撰寫程式時,不能一廂情願地認為錯誤不會發生;應該要考慮可能的錯誤,撰寫相對應的程式碼。

    我們在本文中介紹 C 語言的錯誤處理模式。

    常見的錯誤處理模式

    一般來說,錯誤處理有兩種模式,一種是用 try ... catch ... 或同義的特化控制結構來處理錯誤。像是以下假想的 Python 程式碼:

    try:
        do_something()
    except:
        sys.stderr.write("something wrong\n")
        exit(1)

    try: 區塊中,如果捕捉到錯誤時,會中止其程式的執行,並跳到 except: 區塊中。所以,try ... catch ... 其實是一種受限制的 goto 的變形。

    另外一種是用內建的控制結構來處理錯誤。像是以下假想的 Go 程式碼:

    val, err := doSomething()
    
    // // Check possible error.
    if err != nil {
        panic("Something wrong")
    }

    在此例中,檢查 err 是否為 nil (空值),當 err 不為空值時,進行相對應的動作。

    在 C 語言中,沒有內建的 try ... catch ... 控制結構,使用一般的控制結構來處理錯誤,類似第二種方式。但 C 語言本身沒有錯誤物件 (error object) 的概念,通常是藉由回傳常數來代表錯誤的狀態。

    使用控制結構處理運行期錯誤

    C 語言使用一般的控制結構來處理錯誤。像是以下建立堆疊物件的程式:

    stack_t *s = (stack_t *) malloc(sizeof(stack_t));
    
    // Check possible error.
    if (!s) {
        // Handle error here.
    }

    配置記憶體實際上是有可能失敗的動作,所以我們要在配置記憶體後檢查物件 s 是否為空。在此處,我們使用一般的 if 敘述來處理錯誤。

    goto 很適合用在錯誤處理,因為 goto 和例外很像,都會中斷目前程式執行的流程,直接跳到錯誤處理的程式碼區塊。以下是一個假想的例子:

    #include <stdio.h>
    #include <stdlib.h>
    
    int main(void)
    {
        int *i_p = (int *) malloc(sizeof(int));
        if (!i_p) {
            perror("Failed to allocate int\n");
            goto ERROR;
        }
        
        // Do more things here.
        
        free(i_p);
    
        return 0;
    
    ERROR:
        if (i_p)
            free(i_p);
        
        return 1;
    }

    在這個範例中,當程式發生錯誤時,會中斷目前的程式,跳到 ERROR 標籤所在的位置。在 ERROR 標籤所在的程式,我們會釋放系統資源,並在最後回傳代表程式異常結束的 1。在這個範例中,goto 在精神上類似於拋出例外。

    使用 exitabort 函式中止程式

    exit() 函式和 abort() 函式的用途皆為提早結束程式。兩者的差別在於 exit() 會完成清理的動作後再結束程式,而 abort() 則會立即結束程式。這個和拋出例外 (exception) 意義不同,因為呼叫這兩個函式後程式會中止,我們無法接住這個事件,所以除了嚴重的錯誤外,不應隨意呼叫這兩個函式。

    利用 assert 檢查程式錯誤

    assert 巨集的用途是在開發過程中確認程式是否有誤,如下例:

    int stack_pop(stack_t *self)
    {
        assert(!stack_is_empty(self));
        
        Node *temp = self->top;
        int popped = temp->data;
        
        self->top = temp->next;
        free(temp);
        
        return popped;
    }

    由於 assert 可以藉由編譯參數手動關閉,我們可以自己製作等效的巨集:

    #ifndef assert
        #include <stdio.h>
        #define assert(cond) { \
            if (!(cond)) { \
                fprintf(stderr, "(%s:%d): Failed on %s\n", __FILE__, __LINE__, #cond); \
                exit(1); \
            } \
        }
    #endif

    setjmplongjmp

    其實,C 語言也有類似例外 (exception) 的語法特性,就是透過 setjmp.h 函式庫的 setjmp() 函式和 longjmp() 函式。實例如下:

    #include <stdio.h>
    #include <setjmp.h>
    
    int main(void) {
        jmp_buf buf;
    
        if (!setjmp(buf)) {
            printf("Something wrong");
        } else {
            longjmp(buf, 1); // Jump to `setjmp` with new `buf`
            
            printf("Hello World!\n");
        }
    
        return 0;
    }

    我們先用 setjmp() 函式設置接收 jump 的點,在後續的程式中用 longjmp() 觸發 jump,程式就會跳回 jump 所設的位置。以本程式來說,該程式會印出 "Something wrong" 而不會印出 "Hello World"

    國外已有聰明的開發者利用這項特性模擬出 try ... catch ... 區塊了,詳見下一節。

    模擬 try ... catch ... 區塊

    這個程式的原始出處在這裡,有興趣的讀者可以看一看,本節展示其用法。

    先建立以下的巨集:

    #ifndef _TRY_THROW_CATCH_H_
    #define _TRY_THROW_CATCH_H_
    
    #include <stdio.h>
    #include <setjmp.h>
    
    #define TRY do { jmp_buf ex_buf__; switch( setjmp(ex_buf__) ) { case 0: while(1) {
    #define CATCH(x) break; case x:
    #define FINALLY break; } default: {
    #define ETRY break; } } }while(0)
    #define THROW(x) longjmp(ex_buf__, x)
    
    #endif /*!_TRY_THROW_CATCH_H_*/

    實際套用該巨集的程式如下:

    #include <stdio.h>
    // Include the above library.
    #include "try_catch.h"
    
    int main(void)
    {
        TRY
            THROW(2);
            printf("Hello World\n");
        CATCH(1)
            printf("Something wrong\n");
        CATCH(2)
            printf("More thing wrong\n");
        CATCH(3)
            printf("Yet another thing wrong\n");
        FINALLY
            printf("Clean resources\n");
        ETRY
    
        return 0;
    }

    實際執行程式的效果如下:

    $ gcc -o file file.c
    $ ./file
    More thing wrong
    Clean resources

    讀者可能會覺得很神奇,不知如何做到的。我們利用 GCC 將前置處理器處理後的結果展開女下:

    int main(void)
    {
        do {
    	    jmp_buf ex_buf__;
    	    switch (setjmp(ex_buf__)) {
    	    case 0:
    	        while (1) {
    		    longjmp(ex_buf__, 2);
    		    printf("Hello World\n");
    		    break;
    	    case 1:
    		    printf("Something wrong\n");
    		    break;
    	    case 2:
    		    printf("More thing wrong\n");
    		    break;
    	    case 3:
    		    printf("Yet another thing wrong\n");
    		    break;
    	        }
    	    default:{
    		    printf("Clean resources\n");
    		    break;
    	        }
    	    }
        } while (0);
    
        return 0;
    }

    可以發現其實整個程式是包在一個 switch 敘述中,藉由調整 ex_buf__ 的值來控制程式行進的方向。用巨集模擬語法其實算是 C 語言的一種反模式 (anti-pattern),要不要使用這樣的巨集就由讀者自行決定。

    【分享本文】
    Facebook Twitter LinkedIn LINE Skype EverNote GMail Yahoo Email
    【追蹤新文章】
    Facebook Twitter Plurk