Switch语句:默认情况必须是最后一个case吗?

213

考虑下面的switch语句:

switch( value )
{
  case 1:
    return 1;
  default:
    value++;
    // fall-through
  case 2:
    return value * 2;
}

这段代码可以编译,但它对于C90/C99是否有效(即定义良好的行为)呢?我从未见过默认情况不是最后一种情况的代码。

编辑:
正如Jon CageKillianDS所写:这真的是丑陋和令人困惑的代码,我非常清楚。我只是对一般语法(是否已定义?)和预期输出感兴趣。


24
+1 从未考虑过那种行为。 - Jamie Wong
5
不,顺序并不重要- 如果值与任何情况标签中的常量匹配,则控制将跳转到该标签后面的语句,否则如果有默认情况标签,则控制将跳转到该标签后面的语句。 - Pete Kirkham
14
goto并非邪恶,那些盲目追随的人才是!你无法想象人们为了避免使用goto会走到什么地步,因为它被声称是邪恶的,从而让代码变得难以阅读。 - Patrick Schlüter
1
也许“邪恶”是一个不好的选择,也许“容易被滥用”或“容易被误解”。避免使用goto并不意味着代码可读性降低,通常情况下,如果你需要使用goto,我建议这表明代码在结构上可以更好地组织。如果您有任何反驳我的观点的具体例子,我会非常感兴趣。我并不是在抨击,只是过去曾经因为goto和类似的语言结构而修复了一些严重的错误 :-) - Jon Cage
3
我主要使用 goto 来模拟函数中类似于 finally 的代码块,用于在停止时释放资源(文件、内存),而对于每个错误情况都重复编写一系列的 freeclose 语句并不利于可读性。但是有一种情况我想避免使用 goto 却无法做到,那就是当我想要从一个循环中跳出,并且我正在该循环内部的 switch 中。 - Patrick Schlüter
显示剩余5条评论
12个回答

110

switch语句中case语句和default语句的顺序可以任意排列。如果在case语句中没有常量被匹配,则会执行default语句(可选)。

良好的示例:

switch(5) {
  case 1:
    echo "1";
    break;
  case 2:
  default:
    echo "2, default";
    break;
  case 3;
    echo "3";
    break;
}

输出 2, default

如果你希望在代码中以逻辑顺序呈现你的case(例如,不是按照case 1、case 3、case 2/default的顺序),并且你的case非常长,因此你不想在底部重复整个case代码以处理default情况,那么这个方法非常有用。


11
通常情况下,我会将默认值放在末尾以外的其他位置... 显式情况有逻辑顺序 (1、2、3),我希望默认值的行为与其他不是最后一个显式情况的情况完全相同。 - ArtOfWarfare

94

C99标准对此并没有明确说明,但综合所有事实而言,这是完全有效的。

一个casedefault标签等效于一个goto标签。请参见6.8.1标记语句。特别有趣的是6.8.1.4,它使得已经提到的Duff's Device成为可能:

任何语句都可以在前面加上一个前缀来将标识符声明为标签名称。标签本身不会改变控制流,它们之间的控制流仍将继续不受影响。

编辑:switch内部的代码并没有什么特殊之处;它和if语句中的普通代码块一样,只是多了一些跳转标签。这就解释了为什么会出现“落入”(fall-through)行为,并且为什么需要使用break

6.8.4.2.7甚至给出了一个例子:

switch (expr) 
{ 
    int i = 4; 
    f(i); 
case 0: 
    i=17; 
    /*falls through into default code */ 
default: 
    printf("%d\n", i); 
} 

在这个人工程序片段中,标识符为i的对象具有自动存储期(在块内),但从未被初始化。因此,如果控制表达式具有非零值,则对printf函数的调用将访问不确定的值。类似地,无法到达对函数f的调用。

在switch语句中,case常量必须是唯一的:

6.8.4.2.3每个case标签的表达式都应该是一个整数常量表达式,在同一switch语句中的任何两个case常量表达式在转换后的值不能相同。在一个switch语句中最多只能有一个默认标签。

所有case都会被评估,然后跳转到默认标签(如果给出):

6.8.4.2.5 在控制表达式上执行整数提升。每个case标签中的常量表达式转换为控制表达式提升类型的类型。如果转换后的值与提升的控制表达式的值匹配,则控制跳转到匹配的case标签后的语句。否则,如果有默认标签,则控制跳转到标记语句。如果没有转换的case常量表达式匹配并且没有默认标签,则不执行switch体的任何部分。


8
@HeathHunnicutt ,显然你没有理解这个例子的目的。这段代码不是由此贴文的作者编写的,而是直接从C标准中提取出来的,用来说明switch语句有多奇怪以及糟糕的编码习惯会导致bug。如果你认真阅读了代码下面的文字,你就会意识到这一点。 - Lundin
4
+1 来补偿那个被踩的人。因为援引 C 标准而对别人进行踩票评价似乎有些过分了。 - Lundin
2
@Lundin 我并没有对C标准进行贬低,也没有像你所说的那样忽略任何东西。我是对使用一个不好且不必要的例子来进行糟糕的教学方式进行了贬低。特别是,那个例子与被问及的完全不同。我可以继续说下去,但“感谢您的反馈”。 - Heath Hunnicutt
14
英特尔告诉你在 switch 语句中将最频繁的代码放在第一位,以防止错误预测。详情请见分支和循环重组以防止错误预测。我在这里是因为我的 default 情况比其他情况多约100倍,我不知道将 default 放在第一种情况是否有效或未定义。 - jww
@jww 我不确定您所说的 Intel 是什么意思。如果您是指智能,则我将称其为假设。我曾经也有过同样的想法,但后来阅读了一些资料,发现与 if 语句不同,switch 语句是随机访问的。因此,最后一个 case 到达的速度并不比第一个慢。这是通过对常量 case 值进行哈希实现的。这就是为什么当分支很多时,switch 语句比 if 语句更快的原因。 - user9599745
1
@JaveneCPPMcGowan 我相信Intel jww指的是生产计算机芯片的公司,而不是普通的intelligence一词。而你的想法并不完全正确:编译器会自行将某些开关情况转换为参数化(寄存器间接)无条件jmp,并将其他开关情况转换为条件分支。 - 把友情留在无盐

57
在某些情况下,它是有效且非常有用的。
考虑以下代码:
switch(poll(fds, 1, 1000000)){
   default:
    // here goes the normal case : some events occured
   break;
   case 0:
    // here goes the timeout case
   break;
   case -1:
     // some error occurred, you have to check errno
}

重点是上述代码比级联的if更易读且更高效。你可以将default放在最后,但这没有意义,因为它会让你关注错误情况而不是正常情况(这里是default情况)。
实际上,这不是一个很好的例子,在投票中,你知道最多可能发生多少事件。我的真正意图是存在一组定义好的输入值的情况,其中存在'异常'和正常情况。将异常或正常情况放在前面更好,这是个人选择问题。
在软件领域中,我想到另一个非常常见的情况:具有一些终止值的递归。如果你可以使用switch表达它,default将是包含递归调用和区分元素(单独的情况)的通常值,通常不需要关注终止值。
另一个原因是情况的顺序可能会改变编译后的代码行为,这对性能很重要。大多数编译器将按照switch中出现的代码顺序生成编译后汇编代码。这使得第一个情况与其他情况非常不同:除了第一个情况外,所有情况都涉及跳转并清空处理器管道。你可以理解为分支预测器默认运行switch中首次出现的情况。如果一个情况比其他情况更常见,那么你有非常好的理由将其作为第一个情况。
阅读评论是原帖提出该问题的具体原因,读完Intel编译器分支循环重组关于代码优化的内容后。然后就会成为代码可读性和代码性能之间的某种仲裁。可能最好在第一个案例出现时加上注释,以便将来的读者理解。

8
+1 对于提供一个(好的)例子而言,没有出现任何错误行为。 - KillianDS
1
然而,经过考虑,我不认为将默认设置在顶部是好的,因为很少有人会在那里寻找它。分配返回值给一个变量,在if的一侧处理成功,在另一侧使用case语句处理错误可能更好。 - Jon Cage
@Jon:直接写就好了。你加入的语法噪音没有任何可读性的好处。而且,如果默认值在顶部,真的没有必要去看它,这是非常明显的(如果你把它放在中间可能会更棘手)。 - kriss
@Andrew Grimm:C语言在大多数情况下是不敏感于空格的(这里的“大多数”是因为有些情况下需要至少一个空格来分隔两个单词,比如int v;)。实际上,为了让我的缩进保持一致,default应该与case对齐。我会编辑我的答案,感谢您的评论。 - kriss
1
@kriss:我有一半的冲动想说“我也不是Python程序员!” :) - Andrew Grimm
显示剩余6条评论

20

是的,这是有效的,在某些情况下甚至很有用。通常情况下,如果不需要它,就不要这么做。


-1:这让我感觉有问题。最好将代码拆分为一对switch语句。 - Jon Cage
30
@John Cage:在这里给我打-1是很不好的。这不是我的错,这是有效的代码。 - Jens Gustedt
只是好奇,我想知道在哪些情况下它是有用的? - Salil
1
如果你能提供一个有效的例子来支持你的说法,我会把它改成+1。而-1是针对你认为它有用的断言。 - Jon Cage
4
有时候,在切换执行过程中,我们会遇到一些系统函数返回的错误码(errno)。比如说我们有一个场景,在这个场景下我们知道必须要做完整的退出操作,但是这个操作可能需要编写一些我们不想重复的代码行。另外,我们还可能会遇到很多其他的异常错误码,我们并不想单独处理它们。我建议在默认情况下设置一个perror并让程序继续运行到其他地方并完成干净的退出操作。当然,我并不是说你应该像这样做,这只是一种个人偏好。 - Jens Gustedt
@Salil LLVM 在许多情况下使用 default 作为避免分支预测失败的方法。不幸的是,LLVM 的实现是基于 C++ 的,这可能与 C 标准有所不同。无论如何,您可以在以下链接中进行查看:https://reviews.llvm.org/rL315525 - yeyo

11

在switch语句中没有定义的顺序。你可以把它看作是类似于goto标签的命名标签。与人们在这里所认为的相反,在值为2的情况下,不会跳转到默认标签。为了举一个经典的例子,这里有达夫设备(Duff's device),它是C语言中switch/case的极端范例。

send(to, from, count)
register short *to, *from;
register count;
{
  register n=(count+7)/8;
  switch(count%8){
    case 0: do{ *to = *from++;
    case 7:     *to = *from++;
    case 6:     *to = *from++;
    case 5:     *to = *from++;
    case 4:     *to = *from++;
    case 3:     *to = *from++;
    case 2:     *to = *from++;
    case 1:     *to = *from++;
            }while(--n>0);
  }
}

6
对于不熟悉达夫设备的人来说,这段代码完全无法理解。 - KillianDS
达夫设备是“你能做到并不意味着你应该这样做”的完美例子。 - Andrew

9

有一种情况下,我认为在 switch 语句中将 default case 放在其他位置是适当的,那就是在状态机中,当出现无效状态时应该重置状态机,并像初始状态一样继续执行。例如:

switch(widget_state)
{
  default:  /* Fell off the rails--reset and continue */
    widget_state = WIDGET_START;
    /* Fall through */
  case WIDGET_START:
    ...
    break;
  case WIDGET_WHATEVER:
    ...
    break;
}

如果无效状态不应该重置机器,但应该容易识别为无效状态,则可以采用另一种排列方式:
switch(widget_state)
{
  case WIDGET_IDLE:
    widget_ready = 0;
    widget_hardware_off();
    break;
  case WIDGET_START:
    ...
    break;
  case WIDGET_WHATEVER:
    ...
    break;
  default:
    widget_state = WIDGET_INVALID_STATE;
    /* Fall through */
  case WIDGET_INVALID_STATE:
    widget_ready = 0;
    widget_hardware_off();
    ... do whatever else is necessary to establish a "safe" condition
}

其他地方的代码可以检查 widget_state == WIDGET_INVALID_STATE 并提供任何适当的错误报告或状态重置行为。例如,状态栏代码可以显示错误图标,并且在大多数非空闲状态下禁用的 "启动小部件" 菜单选项也可以针对 WIDGET_INVALID_STATEWIDGET_IDLE 启用。


6

再举一个例子:如果“默认值”是一个意外情况,而你想记录错误但又想做一些明智的事情,那么这会很有用。以下是我自己代码的示例:

  switch (style)
  {
  default:
    MSPUB_DEBUG_MSG(("Couldn't match dash style, using solid line.\n"));
  case SOLID:
    return Dash(0, RECT_DOT);
  case DASH_SYS:
  {
    Dash ret(shapeLineWidth, dotStyle);
    ret.m_dots.push_back(Dot(1, 3 * shapeLineWidth));
    return ret;
  }
  // more cases follow
  }

5

有时候你需要将枚举类型转换成字符串,或者将字符串转换成枚举类型,在写入/读取文件时使用。

有时候你需要将其中一个值设置为默认值,以纠正手动编辑文件所造成的错误。

switch(textureMode)
{
case ModeTiled:
default:
    // write to a file "tiled"
    break;

case ModeStretched:
    // write to a file "stretched"
    break;
}

3

default条件可以在任何case语句存在的位置。它不需要是最后一个条件。我看过一些将default作为第一个条件的代码。即使default在其上方,case 2:也会被正常执行。

为了测试,我将示例代码放入一个函数中,命名为test(int value){}并运行:

  printf("0=%d\n", test(0));
  printf("1=%d\n", test(1));
  printf("2=%d\n", test(2));
  printf("3=%d\n", test(3));
  printf("4=%d\n", test(4));

输出结果为:
0=2
1=1
2=4
3=8
4=10

1

这是有效的,但相当恶劣。我建议不要允许穿透,因为它可能会导致非常混乱的代码。

最好将这些情况分解成几个switch语句或更小的函数。

[编辑] @Tristopia:你的示例:

Example from UCS-2 to UTF-8 conversion 

r is the destination array, 
wc is the input wchar_t  

switch(utf8_length) 
{ 
    /* Note: code falls through cases! */ 
    case 3: r[2] = 0x80 | (wc & 0x3f); wc >>= 6; wc |= 0x800; 
    case 2: r[1] = 0x80 | (wc & 0x3f); wc >>= 6; wc |= 0x0c0; 
    case 1: r[0] = wc;
}

如果它被写成这样,它的意图会更清晰(我认为):

...

if( utf8_length >= 1 )
{
    r[0] = wc;

    if( utf8_length >= 2 )
    {
        r[1] = 0x80 | (wc & 0x3f); wc >>= 6; wc |= 0x0c0; 

        if( utf8_length == 3 )
        {
            r[2] = 0x80 | (wc & 0x3f); wc >>= 6; wc |= 0x800; 
        }
    }
}   

[编辑2] @Tristopia:你的第二个例子可能是使用跟随的最干净的例子:

for(i=0; s[i]; i++)
{
    switch(s[i])
    {
    case '"': 
    case '\'': 
    case '\\': 
        d[dlen++] = '\\'; 
        /* fall through */ 
    default: 
        d[dlen++] = s[i]; 
    } 
}

但是个人认为应该将评论识别拆分成自己的函数:

bool isComment(char charInQuestion)
{   
    bool charIsComment = false;
    switch(charInQuestion)
    {
    case '"': 
    case '\'': 
    case '\\': 
        charIsComment = true; 
    default: 
        charIsComment = false; 
    } 
    return charIsComment;
}

for(i=0; s[i]; i++)
{
    if( isComment(s[i]) )
    {
        d[dlen++] = '\\'; 
    }
    d[dlen++] = s[i]; 
}

2
有时候,穿透下去确实是一个非常好的想法。 - Patrick Schlüter
以下是UCS-2到UTF-8转换的示例,其中r是目标数组,wc是输入的wchar_t: switch(utf8_length) { /* 注意:代码在各个case之间会穿透! */ case 3: r[2] = 0x80 | (wc & 0x3f); wc >>= 6; wc |= 0x800; case 2: r[1] = 0x80 | (wc & 0x3f); wc >>= 6; wc |= 0xc0; case 1: r[0] = wc; } - Patrick Schlüter
如果你真的需要极快的速度,并且可以证明它不会影响可读性,那么我认为你的方法是正确的。但我会惊讶地发现两者之间有可基准化的差异。 - Jon Cage
1
是的,特别是在保加利亚语和希腊语转换方面(在Solaris SPARC上),以及带有我们内部标记的文本(这是3字节UTF8)。诚然,在总体上它并不多,并且自从我们进行最后一次硬件更新以来已经变得不相关了,但在当时编写它确实起到了一些作用。 - Patrick Schlüter
我在发布之前削减了它们,因为这里有评论限制。话虽如此,这个例子来自我们项目的一个不适合胆小者的部分。其中还有其他一些不太学术的东西,例如分配d缓冲区的语句:d = memcpy(calloc(olen*2+10,1), "echo \"", 6); ;-) - Patrick Schlüter
显示剩余3条评论

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