为什么在switch结构内不能使用“goto default;”或“goto case x;”语句?

21

C11第6.8.1节C99,或C89的第3.6.1节似乎都表明defaultcase x(其中x是一些常量表达式)都是带标签的语句示例,以及适用于与goto一起使用的identifier:样式标签。

我知道我可以直接在default:case x:标签后面放置一个identifier:样式标签。但这个问题不是关于那个的。我更想知道是否有任何实际的理由禁止这种行为。

如果可以在switch选择结构之外声明default:标签,那么我就能理解了,因为在switch选择结构内部的goto的目的地会产生一些冲突。然而,C11的6.4.1节C99C89的3.1.1节禁止将default用作除关键字以外的任何内容,并且6.8.1进一步限制其仅适用于switch结构(或C11中的generic结构,在此处无关)。

我也理解,如果有多个(可能嵌套)的switch结构,每个结构都有default:(或case x:)标签会引入歧义,但是这些标签的作用范围似乎仅限于它们最内层的switch结构中,并且引用其作用域之外的任何标识符都明显是一个错误,需要在编译时进行诊断。
这个问题在任何标准文档(例如理由)中有讨论吗?除了“因为它就是这样”或“因为规范这样说”之外,还有什么解释这种行为的方式吗?如果有,那是什么解释?

7
如果你的推理是“goto可能很危险,因此不应该使用”,那么请考虑“C语言中的任何功能都可能很危险,因此不应该使用”或者“驾车可能很危险...”。这样说有意义吗?引发“goto很糟糕”的浪潮的那句话最初由艾兹赫尔·迪科斯彻写下,但已被像你一样的人断章取义地使用了几十年,以至于他很久以前就对此进行了评论并表示后悔。 - autistic
2
@molbdnilo FYI,原始上下文更多的是“如果有更合适的控制结构(例如if/elsewhile/do-whileswitch和函数),那么应该使用它们”...然而,在C语言中仍然存在一小部分用例,其中goto是最合适的控制结构,最简单的情况是跳转到函数末尾的级联资源清理以进行错误处理或者跳出多个嵌套循环/开关控制结构,而不需要额外的逻辑。 - autistic
@molbdnilo...作为一个更复杂的合理使用goto的例子,请参见David Tribble的结论中所述的“goto之道” - autistic
是的 - goto 被认为是一种有用的级联错误标签的方法,以确保(以简单且不重复的方式)在出现错误时所有本地分配/资源请求都被展开。 - amdixon
4个回答

6

我不明白在语法上如何使用goto跳转到一个case

就像您所说的,casedefault标签仅具有相应switch语句的范围,并且只能从外部跳转到它们。另一方面,在C中,标签具有函数范围,并且可以从函数的任何位置跳转到它们。

因此,我们正在讨论具有非常不同属性的标签,它们可能在内部被处理得很不同。似乎相对复杂地调和这些属性并会使实现变得更加复杂。突然间,你必须要决定在实现goto时哪个casedefault是正确的,而现在你只需要把文件范围标识符的地址放在那里。

尽管如此,这只是关于这种区别的原始意图的猜测。但我确定,以这样的推理去争论将迅速扼杀任何现在引入这种功能的尝试。


10
在C#中,由于禁止switch语句的case穿透,因此您可以使用goto case x;明确跳转到一个标签。 - phuclv
1
这个答案很好,也是我的猜测,但实现“goto”并不会使它变得更加困难。如果有的话,我真正想要的(并且会得到我的采纳答案)是从解释中引用或其他什么东西... - autistic
这不仅仅是作用域的问题;case和default标签只不过是goto语句的非法目标。而且你总是可以编写case somecase: somecase: ... goto somecase; - gnasher729
@gnasher729,你看到这个问题是涉及C标准的吗?你认为你能找到一些官方的东西来支持你的说法吗? - autistic
作为对下面回答的回应,我写了这段代码:{ int x = 42; { int x = -1; } }。在这个嵌套块中第一个声明之后的任何时刻,你都可以声明另一个 int 并将其赋值给 x……那么,是哪个 x?好吧,在 C 中有关于变量遮蔽的规则。 - autistic
@gnasher729 嗯,是的,手动定义与 switch 的每个分支对应的标签始终是一种选择。请注意,该标签在函数中只能存在一次,因此,如果您想稍后再次添加相同的 switch(其中包含不同的逻辑),则会出现问题。此外,由于没有 default 的同义词,而 case default: default: ... goto default; 是非法的,因此编写与称为“默认”操作的最终用户概念相关的可维护代码变得非常困难;这成为了一个“什么是最不令人困惑的同义词”的游戏。你查过了吗?我查过了。 - autistic

5

反例

fun() {
  switch (b) {
     case x:
       doSomething();
       break;
  }
  switch(b2) {
     case x:
       doMore();
       break;
  }

  goto x; // which one?
}

13
他们俩都不符合要求。你有没有读完整个问题? - autistic
更具体地说,我指的是像这样的示例,其中 case x 仍然在作用域内:switch (b) { case x: doSomething(); default: goto case x; /* which one? */ } switch (b2) { case x: doMore(); default: goto case x; /* which one? */ }评论中的问题的答案应该是显而易见的。 - autistic
1
{ int x = 42; { int x = -1; } } int y = x; // which one? - autistic
1
要明确一点,我并不是在支持你所认为的那个。哪一个是无关紧要的,因为两者的范围都不存在于这些开关之外,因此仍然会出现错误(与我给出的反驳示例属于同一类别的错误)。casedefault仍将被限制在其各自的switch语句中,因此您只能从switch内部goto default。这有意义吗?至少接受的答案解决了这个问题。 - autistic
我还想指出,我很清楚有一个简单的“解决方案”:定义您自己的标签名称,有时使用“default”作为标签名称是有道理的,对吗?您能想到“default”的同义词吗?因为我有点想使用它,但苦苦思索了一段时间... 祝好运 ;) - autistic

2

ISO/IEC 9899:2011 信息技术 -- 程序语言 -- C

6.8.1 标号语句

  1. 语法

标号语句:

标识符 : 语句

case 常量表达式 : 语句

default : 语句

  1. case或default标签只能出现在switch语句中。有关这些标签的进一步限制,请参见switch语句。
  2. 标签名在函数内必须唯一。
  3. 任何语句都可以在前面加上一个前缀,以声明标识符为标签名。标签本身不会改变控制流,控制流将不受其影响而继续进行。

6.8.6 跳转语句

  1. 语法

跳转语句:

goto 标识符 ;

6.8.6.1 goto语句

  1. goto语句中的标识符应命名为在封闭函数中某处的标签。
  2. goto语句导致无条件跳转到封闭函数中的命名标签所在的语句。

6.2.1 标识符的作用域

  1. 标签名是唯一具有函数作用域的标识符类型。它可以在函数内(在goto语句中)任何地方使用,并且通过其语法外观(后跟一个:和语句)隐式声明。

casedefault标签不是标识符,也不是命名标签

"因为规范这么说。" 不要过度思考。


0
我知道我可以直接在default:或case x:标签后面放置一个identifier:风格的标签。但这不是我的问题所在。我更想知道是否有任何实际的理由禁止此类行为。
回答这个问题可能有些啰嗦,原因是每个case都应该从一个单独的goto中跳入(由switch语句创建)。这也是为什么如果我们希望在多个条件下执行一段代码块,就需要将更多的case标签分组在一起的原因。
下面的代码可能会给你一个视觉上的答案。如果允许goto [case label],你会怎样写才能转到第一个case,goto case 'q'还是goto case 'e'?哪一个是那个代码块的“老大”?
#include <stdio.h>

int yes_or_no (char input, char additional_yes) {

    int retval;

    switch (input) {

        case 'q':
        case 'e':

            printf("Bye bye! :-)\n");
            retval = 0;
            break;

        case 'n':

            printf("I am very sad that you said no :-(\n");
            retval = 0;
            break;

        case 'y':
        other_yes:

            printf("Thank you for saying yes! :-)\n");
            retval = 0;
            break;

        case '\n':
        case '\r':
        case ' ':
        case '\t':
        case '\v':
        case '\f':

            printf("Mmmmm... maybe you need time to think...\n");
            retval = 1;
            break;

        default:

            if (input == additional_yes) {

                goto other_yes;

            }

            printf("\"%c\" is not an answer!\n", input);
            retval = 1;


    }

    return retval;

}

int main (int argc, char *argv[]) {

    printf("Please, answer (yes/ja or no/nein) -- e or q to quit\n");

    while (yes_or_no(getchar(), 'j'));

    return 0;

}

考虑到编译器在优化时可能会决定不使用跳转表,如果它认为一个简单的重复if比开关更快(当case标签不多时,这种情况可能发生)。在这种情况下,如果允许goto [case label],你将强制编译器创建一个通常不存在的标签。
这是我最喜欢C语言的一点:如果你想要什么,你必须说出来。

1
在你的答案中,你可能想避免问反问句,因为在这种情况下答案是显而易见的。x: y: // which one can you goto? x, y, or both? 答案是 都不是 boss。最重要的是请注意,case 标签实际上是标签,由 C 标准定义,在 switch 语句中的条件只会被评估一次,在 if-else-if 的链式结构中将被多次评估。你曾经尝试过编写一个 C 编译器吗? - autistic
我注意到你在将getchar返回的int直接转换为char之前没有先检查它是否为EOF... tsssk tsssk,这是一个常见的错误。C语言的一个主要陷阱是:有时编译器和你对编译器的理解不一致。如果你不小心,有时会引发未定义行为,而与其他语言不同,你会得到像heartbleed这样的漏洞,而不是错误消息。现实检验:如果你是C语言的新手(即六个月或更短时间),为什么要学习它? - autistic
是的,它确实如此。EOF不是字符值;它是getchar返回的字符值之外的一个值(如果您阅读手册,则都是unsigned char,因此与负值EOF不同,因此负值EOF不是字符值)。当您将返回值直接转换为字符值时,您正在丢弃EOF不是字符值的事实,并将其视为字符值... 您应该少猜测,多阅读 - autistic
它们并不是那么神奇,它们被称为位掩码。我相信你一定读过它们的相关资料 :) - madmurphy
我往往不太相信缺乏内部文档的软件,尤其是在这么严肃的情况下...你可能信任它,因为你写了它,但这并不对其他人具有传递性。我只是说一下。 - autistic
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接