6. goto语句和标号

分支、循环都讲完了,现在只剩下最后一种影响控制流程的语句了,就是goto语句,实现无条件跳转。我们知道break只能跳出最内层的循环,如果在一个嵌套循环中遇到某个错误条件需要立即跳出最外层循环做出错处理,就可以用goto语句,例如:

  1. for (...)
  2. for (...) {
  3. ...
  4. if (出现错误条件)
  5. goto error;
  6. }
  7. error:
  8. 出错处理;

这里的error:叫做标号(Label),任何语句前面都可以加若干个标号,每个标号的命名也要遵循标识符的命名规则。

goto语句过于强大了,从程序中的任何地方都可以无条件跳转到任何其它地方,只要在那个地方定义一个标号就行,唯一的限制是goto只能跳转到同一个函数中的某个标号处,而不能跳到别的函数中[11]。滥用goto语句会使程序的控制流程非常复杂,可读性很差。著名的计算机科学家Edsger W. Dijkstra最早指出编程语言中goto语句的危害,提倡取消goto语句。goto语句不是必须存在的,显然可以用别的办法替代,比如上面的代码段可以改写为:

  1. int cond = 0; /* bool variable indicating error condition */
  2. for (...) {
  3. for (...) {
  4. ...
  5. if (出现错误条件) {
  6. cond = 1;
  7. break;
  8. }
  9. }
  10. if (cond)
  11. break;
  12. }
  13. if (cond)
  14. 出错处理;

通常goto语句只用于这种场合,一个函数中任何地方出现了错误条件都可以立即跳转到函数末尾做出错处理(例如释放先前分配的资源、恢复先前改动过的全局变量等),处理完之后函数返回。比较用goto和不用goto的两种写法,用goto语句还是方便很多。但是除此之外,在任何其它场合都不要轻易考虑使用goto语句。有些编程语言(如C++)中有异常(Exception)处理的语法,可以代替gotosetjmp/longjmp的这种用法。

回想一下,我们在第 4 节 “switch语句”学过casedefault后面也要跟冒号(:号,Colon),事实上它们是两种特殊的标号。和标号有关的语法规则如下:

语句 → 标识符: 语句
语句 → case 常量表达式: 语句
语句 → default: 语句

反复应用这些语法规则进行组合可以在一条语句前面添加多个标号,例如在例 4.2 “缺break的switch语句”的代码中,有些语句前面有多个case标号。现在我们再看switch语句的格式:

switch (控制表达式) {
case 常量表达式: 语句列表
case 常量表达式: 语句列表

default: 语句列表
}

{}里面是一组语句列表,其中每个分支的第一条语句带有casedefault标号,从语法上来说,switch的语句块和其它分支、循环结构的语句块没有本质区别:

语句 → switch (控制表达式) 语句
语句 → { 语句列表 }

有兴趣的读者可以在网上查找有关Duff’s Device的资料,Duff’s Device是一段很有意思的代码,正是利用“switch的语句块和循环结构的语句块没有本质区别”这一点实现了一个巧妙的代码优化。


[11] C标准库函数setjmplongjmp配合起来可以实现函数间的跳转,但只能从被调用的函数跳回到它的直接或间接调用者(同时从栈空间弹出一个或多个栈帧),而不能从一个函数跳转到另一个和它毫不相干的函数中。setjmp/longjmp函数主要也是用于出错处理,比如函数A调用函数B,函数B调用函数C,如果在C中出现某个错误条件,使得函数BC继续执行下去都没有意义了,可以利用setjmp/longjmp机制快速返回到函数A做出错处理,本书不详细介绍这种机制,有兴趣的读者可参考[APUE2e]