切换语句:默认值是否必须是最后一种情况?


178

考虑以下switch语句:

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

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

编辑:
正如乔恩·凯奇Jon Cage)基里安(KillianDS)写道:这确实很丑陋和令人困惑的代码,我对此很清楚。我只对通用语法(是否已定义?)和预期的输出感兴趣。


19
+1甚至从未考虑过这种行为
Jamie Wong 2010年

@PéterTörök:您的意思是,如果value == 2,它将返回6?
Alexandre C.

4
@PéterTörök不,顺序无关紧要-如果value在任何情况下都匹配常量标签,则控件将跳转到该标签后面的语句,否则控件将跳转到默认标签之后的语句(如果存在)。
Pete Kirkham

11
@乔恩·凯奇goto不是邪恶的。货运崇拜者是!您无法想象人们可以避免什么极端情况,goto因为它是如此地邪恶,使他们的代码真正混乱了。
PatrickSchlüter2010年

3
我使用goto主要是为了模拟像一个finally函数,其中ressources(文件,内存)有停止的时候,并重复每一个错误情况的列表被释放条款freeclose没有帮助的可读性。goto我想避免但不能避免的一种用法是,当我想脱离循环而又switch处于该循环内时。
PatrickSchlüter2010年

Answers:


83

C99标准对此并不明确,但综合所有事实,它是完全有效的。

A casedefaultlabel等同于goto标签。请参见6.8.1带标签的语句。6.8.1.4尤其有趣,它启用了已经提到的Duff设备:

任何声明都可以在前缀之前声明一个标识符作为标签名称。标签本身不会改变控制流,控制流在它们之间继续不受阻碍。

编辑:开关内的代码没什么特别的;它是if-statement中的普通代码块,带有附加的跳转标签。这解释了掉线行为以及为什么这样做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。

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

6.8.4.2.3每个case标签的表达式应为整数常量表达式,并且在转换后,同一switch语句中的两个case常量表达式均不得具有相同的值。switch语句中最多可以有一个默认标签。

对所有案例进行评估,然后跳转至默认标签(如果给定):

6.8.4.2.5对控制表达式执行整数提升。在每种情况下,常量表达式都将标签转换为控制表达式的提升类型。如果转换后的值与提升后的控制表达式的值匹配,则控制跳至匹配的case标签后面的语句。否则,如果有默认标签,则控制跳至带标签的语句。如果没有转换后的大小写常量表达式匹配并且没有默认标签,则不会执行开关主体的任何部分。


6
@HeathHunnicutt您显然不了解该示例的目的。该代码不是由此张贴者组成的,而是直接从C标准中提取的,以说明switch语句的用法以及错误的实践会导致bug的方式。如果您不愿意阅读代码下面的文本,那么您会发现很多。
Lundin

2
+1以补偿下降投票。拒绝某人引用C标准似乎很苛刻。
Lundin

2
@Lundin我并没有反对C标准,也没有像您建议的那样忽略任何内容。我否决了使用坏的,不需要的例子的坏教学法。特别是,该示例完全涉及与所询问的情况不同的情况。我可以继续,但是“感谢您的反馈”。
Heath Hunnicutt

12
英特尔告诉您在“ 分支和循环重组”中的switch语句中放置最频繁的代码,以防止错误预测。我在这里是因为我有一个default案例以100:1的比例主导了其他案例,而且我不知道它是否有效还是未定义来构成default第一个案例。
jww

@jww我不确定您所说的英特尔是什么意思。如果您的意思是智力,我将其称为假设。我有同样的想法,但是后来读到说,与if语句不同,switch语句是随机访问。因此,最后一种情况不会比第一种情况慢。这是通过散列常量大小写值来实现的。这就是为什么分支很多时,switch语句比if语句快的原因。

91

case语句和默认语句可以在switch语句中以任何顺序出现。default子句是一个可选子句,如果case语句中的任何常量都不能匹配,则将其匹配。

好的例子 :-

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


Outputs '2,default'

如果您希望案例以代码中的逻辑顺序显示(例如,不说案例1,案例3,案例2 /默认),并且案例很长,所以您不想重复整个案例,这将非常有用默认代码位于底部


7
这正是我通常将默认值放置在末尾以外的地方的情况……显式情况(1、2、3)有逻辑顺序,我希望默认行为与以下明确情况之一完全相同不是最后一个。
ArtOfWarfare 2012年

51

它是有效的,在某些情况下非常有用。

考虑以下代码:

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情况下)。

实际上,这不是一个很好的例子,因为poll您知道最多可能发生多少个事件。我真正的问题是,有有定义的一组那里有“例外”和正常情况下的输入值的情况。如果最好将异常或正常情况放在前面,则可以选择。

在软件领域,我想到了另一种非常常见的情况:具有某些最终值的递归。如果可以使用开关表示它,default则将是包含递归调用和可区分元素(个别情况)的最终值的常用值。通常无需关注最终值。

另一个原因是,案例的顺序可能会更改编译后的代码行为,这对性能很重要。大多数编译器将以与开关中显示的代码相同的顺序生成已编译的汇编代码。这使第一种情况与其他情况大不相同:除第一种情况外,所有情况都将涉及跳转,这将清空处理器管线。您可能会像分支预测程序那样默认理解为在交换机中运行第一个出现的情况。如果一个案例比其他案例更常见,那么您有充分的理由将其作为第一种案例。

阅读评论,这是最初的发帖人在阅读了英特尔编译器Branch Loop重新组织有关代码优化的问题后提出该问题的具体原因。

然后,它将变成代码可读性和代码性能之间的一个仲裁。最好发表评论,以向将来的读者解释为什么首先出现一个案例。


6
+1用于给出没有失败行为的(良好)示例。
KillianDS 2010年

1
...尽管如此,但我不认为在顶部使用默认值是件好事,因为很少有人会在那里寻找它。最好将返回值分配给变量,并在if和if的一侧使用case语句处理成功。
乔恩·凯奇

@Jon:写下来。您添加语法噪声没有任何可读性的好处。而且,如果默认值位于最前面,则实际上无需查看它,这确实很明显(如果将它放在中间可能会更棘手)。
kriss 2010年

顺便说一句,我不太喜欢C开关/大小写语法。我更希望能够在一个案例之后放置多个标签,而不是必须连续放置多个标签case。令人沮丧的是,它看起来像语法糖,并且如果支持的话,也不会破坏任何现有代码。
kriss 2010年

1
@kriss:我很想说“我也不是python程序员!” :)
安德鲁·格林 Andrew Grimm)2010年

16

是的,这是有效的,并且在某些情况下甚至很有用。通常,如果您不需要它,请不要这样做。


-1:这对我来说是邪恶的。最好将代码拆分为一对switch语句。
乔恩·凯奇

25
@约翰·凯奇(John Cage):在这里给我-1是讨厌的。这是有效代码不是我的错。
詹斯·古斯特

只是好奇,我想知道在什么情况下有用吗?
Salil 2010年

1
-1的目的是让您断言它有用。如果您可以提供一个有效的示例来备份您的主张,我会将其更改为+1。
乔恩·凯奇

4
有时,当切换一个errno时,我们会从某些系统功能获得回报。假设有一种情况,我们完全知道必须做一个干净的出口,但是这个干净的出口可能需要一些我们不想重复的代码行。但是,假设我们还有很多其他的异常错误代码,我们不想单独处理。我会考虑仅在默认情况下放一个perror,然后让它运行到另一个情况下并干净地退出。我不是说你应该那样做。这只是一个品味问题。
詹斯·古斯特

8

switch语句中没有定义的顺序。您可以将案例视为命名标签之类的东西goto。与人们在这里的想法相反,在值2的情况下,默认标签没有跳转到。为了用一个经典的例子来说明,这里是Duff的device,它是switch/caseC语言中极端现象的典型代表。

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);
  }
}

4
对于不熟悉Duff设备的人,此代码是完全不可读的……
KillianDS 2010年

7

在我认为适合的情况下,在case语句的末尾以外的其他位置放置“默认值”的情况是在状态机中,在该状态机中,无效状态应重置该计算机并像初始状态一样继续进行操作。例如:

开关(widget_state)
{
  默认值:/ *脱离轨道-重置并继续* /
    widget_state = WIDGET_START;
    / *跌落* /
  案例WIDGET_START:
    ...
    打破;
  案例WIDGET_WHATEVER:
    ...
    打破;
}

一种替代方案,如果无效状态不应重置机器,但应易于识别为无效状态:

开关(widget_state) { 案例WIDGET_IDLE: widget_ready = 0; widget_hardware_off(); 打破; 案例WIDGET_START: ... 打破; 案例WIDGET_WHATEVER: ... 打破; 默认: widget_state = WIDGET_INVALID_STATE; / *跌落* / 案例WIDGET_INVALID_STATE: widget_ready = 0; widget_hardware_off(); ...为建立“安全”条件做其他必要的事情 }

然后,其他地方的代码可能会检查(widget_state == WIDGET_INVALID_STATE),并提供任何适当的错误报告或状态重置行为。例如,状态栏代码可能显示错误图标,并且可以为WIDGET_INVALID_STATE和WIDGET_IDLE启用在大多数非空闲状态中都禁用的“开始小部件”菜单选项。


6

再举一个例子:如果“ default”是意外情况,并且您想记录错误但又做一些明智的事情,这可能很有用。我自己的一些代码示例:

  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

在某些情况下,如果要在文件中进行读写操作,则将ENUM转换为字符串或将字符串转换为枚举。

有时您需要将其中一个值设为默认值,以覆盖手动编辑文件所导致的错误。

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

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

2

default条件可以在case子句可以存在的开关内的任何位置。不需要成为最后一个子句。我看过将默认值作为第一个子句的代码。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; 
        }
    }
}   

[edit2] @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
在某些情况下,克服失败确实是一个好主意。
PatrickSchlüter2010年

从UCS-2到UTF-8的转换示例r是目标数组,wc是输入wchar_t 开关(utf8_length){/ *注意:代码会遇到各种情况!* /情况3:r [2] = 0x80 | (wc&0x3f); wc >> = 6; wc | = 0x800; 情况2:r [1] = 0x80 | (wc&0x3f); wc >> = 6; wc | = 0xc0; 情况1:r [0] = wc;}
PatrickSchlüter10年

这是另一个带有字符转义符的字符串复制例程: for(i=0; s[i]; i++) { switch(s[i]) { case '"': case '\'': case '\\': d[dlen++] = '\\'; /* fall through */ default: d[dlen++] = s[i]; } }
PatrickSchlüter2010年

是的,但是此例程是我们的热点之一,这是实现它的最快,可移植(我们不会进行组装)的方式。对于任何UTF长度,它只有1个测试,而对于2个或3个,则只有3个。此外,我没有提出,我从BSD那里拿来的。
PatrickSchlüter2010年

1
是的,特别是在保加利亚语和希腊语(在Solaris SPARC中)和带有我们内部标记的文本(3字节UTF8)的转换中。承认,自我们上次硬件更新以来,它并不多,并且已变得无关紧要,但是在编写该文件时,它有所不同。
PatrickSchlüter2010年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.