【在主画面加入捷径】
       
【选择语系】
繁中 简中

[C 语言] 程序设计教学:错误处理 (Error Handling)

【赞助商连结】

    常见的错误处理模式

    即使程序本身没有错误,不代表程序运行时不会发生错误;程序要面对许多程序以外的错误,像是权限不足、文件内容错误、网络无法连线等。一开始学程序设计时,我们会先忽略错误处理的部分,这是为了简化程序代码,易于学习。但我们在撰写程序时,不能一厢情愿地认为错误不会发生;应该要考虑可能的错误,撰写相对应的程序代码。

    一般来说,错误处理有两种模式,一种是用 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 叙述来处理错误。

    使用 exitabort函数中止程序

    exit()函数和 abort()函数的用途皆为提早结束程序。两者的差别在于 exit() 会完成清理的动作后再结束程序,而 abort() 则会立即结束程序。这个和抛出例外 (exception) 意义不同,因为调用这两个函式后程序会中止,我们无法接住 (catch) 这个事件,所以除了严重的错误外,不应随意调用这两个函式。

    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),要不要使用这样的宏就由读者自行决定。

    【赞助商连结】