跳转到内容

6. goto 语句和标号

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

c
for (...)
    for (...) {
        ...
        if (出现错误条件)
            goto error;
    }
error:
    出错处理;

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

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

c
int cond = 0; /* bool variable indicating error condition */
for (...) {
    for (...) {
        ...
        if (出现错误条件) {
            cond = 1;
            break;
        }
    }
    if (cond)
        break;
}
if (cond)
    出错处理;

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

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

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

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

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

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

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

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


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