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)