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)
- The Supervised Learning Workshop
- Visual C++串口通信開發入門與編程實踐
- Getting Started with ResearchKit
- JavaScript+jQuery開發實戰
- Processing互動編程藝術
- 假如C語言是我發明的:講給孩子聽的大師編程課
- STM32F0實戰:基于HAL庫開發
- Microsoft System Center Orchestrator 2012 R2 Essentials
- HTML5入門經典
- Spring Boot企業級項目開發實戰
- Internet of Things with ESP8266
- Scala Data Analysis Cookbook
- Red Hat Enterprise Linux Troubleshooting Guide
- JavaScript動態網頁編程
- Clean Code in C#