官术网_书友最值得收藏!

1.5.2 錯誤處理及集中返回

如前所述,Linux內核的編碼風格要求所有的函數應在末尾提供統一的出口,因此我們在Linux內核的源代碼中看到goto語句被頻繁使用。實際上,除了Linux內核,其他基于C語言的開源軟件也在使用這一經驗性約定寫法。

為了直觀感受這種寫法的優勢,我們來看看程序清單1.3中的代碼。

程序清單1.3 一個哈希表的創建函數

struct pchash_table *pchash_table_new(size_t size,
        pchash_copy_key_fn copy_key, pchash_free_key_fn free_key,
        pchash_copy_val_fn copy_val, pchash_free_val_fn free_val,
        pchash_hash_fn hash_fn, pchash_equal_fn equal_fn)
{
    struct pchash_table *t;
 
    if (size == 0)
        size = PCHASH_DEFAULT_SIZE;
 
    t = (struct pchash_table *)calloc(1, sizeof(struct pchash_table));
    if (!t)
        return NULL;
 
    t->count = 0;
    t->size = size;
    t->table = (struct pchash_entry *)calloc(size, sizeof(struct pchash_entry));
    if (!t->table) {
        free(t);
        return NULL;
    }
 
    t->copy_key = copy_key;
    t->free_key = free_key;
    t->copy_val = copy_val;
    t->free_val = free_val;
    t->hash_fn = hash_fn;
    t->equal_fn = equal_fn;
 
    for (size_t i = 0; i < size; i++)
        t->table[i].key = PCHASH_EMPTY;
 
    if (do_other_initialization(t)) {
        free(t->table);
        free(t);
        return NULL;
    }
 
    return t;
}

上述代碼實現了一個用來創建哈希表的函數pchash_table_new()。在這個函數中,我們需要執行兩次內存分配,一次用于分配哈希表本身,另一次用于分配保存各個哈希項的數組。另外,該函數還調用了一次do_other_initialization()函數,以執行一次額外的初始化操作。如果第二次內存分配失敗,或者額外的初始化操作失敗,則需要釋放已分配的內存并返回NULL表示失敗。可以想象,我們還需要執行其他更多的初始化操作,當后續的任何一次初始化操作失敗時,我們就需要不厭其煩地在返回NULL之前調用free()函數來釋放前面已經分配的內存,否則就會造成內存泄漏。

要想優雅地處理上述情形,可按如下代碼(為節省版面,我們略去了部分代碼)所示使用goto語句,如此便能起到化腐朽為神奇的效果:

struct pchash_table *pchash_table_new(...)
{
    struct pchash_table *t = NULL;
 
    ...
    t = (struct pchash_table *)calloc(1, sizeof(struct pchash_table));
    if (!t)
        goto failed;
 
    ...
    t->table = (struct pchash_entry *)calloc(size, sizeof(struct pchash_entry));
    if (!t->table) {
        goto failed;
    }
 
    ...
 
    if (do_other_initialization(t)) {
        goto failed;
    }
 
    return t;
 
failed:
    if (t) {
        if (t->table)
            free(t->table);
        free(t);
    }
 
    return NULL;
}

以上寫法帶來的好處顯而易見:將函數中多個初始化操作失敗時的處理統一集中到函數末尾,減少了return語句出現的次數,方便了代碼的維護。

還有一個技巧,我們可以通過定義多個goto語句的目標標簽(label),讓以上代碼變得更加簡潔:

struct pchash_table *pchash_table_new(...)
{
    struct pchash_table *t = NULL;
 
    ...
    t = (struct pchash_table *)calloc(1, sizeof(struct pchash_table));
    if (!t)
        goto failed;
 
    ...
    t->table = (struct pchash_entry *)calloc(size, sizeof(struct pchash_entry));
    if (!t->table) {
        goto failed_table;
    }
 
    ...
 
    if (do_other_initialization(t)) {
        goto failed_init;
    }
 
    return t;
 
failed_init:
    free(t->table);
 
failed_table:
    free(t);
 
failed:
    return NULL;
}

以上寫法帶來的好處是,調用free()函數時不再需要作額外的判斷。

在實踐中,我們還可能遇到一種寫法,就是在進行錯誤處理時避免使用有爭議的goto語句,例如:

struct pchash_table *pchash_table_new(...)
{
    struct pchash_table *t = NULL;
 
    do {
        t = (struct pchash_table *)calloc(1, sizeof(struct pchash_table));
        if (!t)
            break;
 
        ...
        t->table = (struct pchash_entry *)calloc(size,
                sizeof(struct pchash_entry));
        if (!t->table) {
            break;
        }
 
        ...
 
        if (do_other_initialization(t)) {
            break;
        }
 
        return t;
    } while (0);
 
    if (t) {
        if (t->table)
            free(t->table);
        free(t);
    }
 
    return NULL;
}

本質上,上述寫法利用了do - while (0)單次循環,因為我們可以使用break語句跳出這一循環,從而避免goto語句的使用。

但筆者并不建議使用這種寫法,原因有二。

(1)大部分人看到do語句的第一反應是循環。在看到while (0)語句之前,很少有人會想到這段代碼本質上不是循環,從而影響代碼的可讀性。

(2)這種寫法額外增加了一次不必要的縮進。這一方面會讓代碼從感官上變得更為復雜,另一方面則會出現因為堅守“80列”這條紅線而不得不繞行的情形。

需要說明的是,在定義宏時,我們經常使用do - while (0)單次循環,尤其是當一個宏由多條語句組成時:

#define FOO(x)                  \
    do {                        \
        if (a == 5)             \
            do_this(b, c);      \
    } while (0)
主站蜘蛛池模板: 赞皇县| 金寨县| 安泽县| 长泰县| 邵东县| 墨脱县| 平泉县| 锡林浩特市| 米泉市| 武威市| 容城县| 刚察县| 郑州市| 榆林市| 双牌县| 志丹县| 鄂伦春自治旗| 循化| 朝阳县| 石柱| 东乡县| 和顺县| 奉化市| 油尖旺区| 恩施市| 南乐县| 双鸭山市| 北辰区| 漳浦县| 涿州市| 独山县| 扎兰屯市| 五指山市| 鹿泉市| 平果县| 紫阳县| 疏附县| 邳州市| 武冈市| 乐昌市| 石泉县|