开关语句:必须默认是最后一种情况?
考虑下面的switch
语句:
switch( value ) { case 1: return 1; default: value++; // fall-through case 2: return value * 2; }
此代码编译,但它是有效的(=定义的行为)的C90 / C99? 我从来没有见过默认情况下不是最后一种情况的代码。
编辑:
正如Jon Cage和KillianDS写的:这真是丑陋而令人困惑的代码,我很清楚这一点。 我只是在一般的语法(是否定义?)和预期的输出感兴趣。
C99标准对此并不明确,但把所有的事实放在一起,这是完全有效的。
case
和default
标签相当于goto
标签。 见6.8.1标记语句。 特别有趣的是6.8.1.4,它使已经提到的Duff's Device:
任何语句都可以在前面声明一个标识符作为标签名称的前缀。 标签本身不会改变控制stream程,控制stream程不受阻碍。
编辑 :一个开关内的代码是没有什么特别的; 它是一个正常的代码块,如在if
语句中,带有额外的跳转标签。 这就解释了破产行为,为什么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的对象存在自动存储持续时间(在块内),但是从不初始化,因此如果控制expression式具有非零值,则对printf函数的调用将访问不确定的值。 类似地,无法达到函数f的调用。
case语句常量在switch语句中必须是唯一的:
6.8.4.2.3每个case标号的expression式应该是一个整数常量expression式,同一个switch语句中的两个case常量expression式在转换后不能有相同的值。 switch语句中最多只能有一个默认标签。
所有的情况下评估,然后跳转到默认的标签,如果给出:
6.8.4.2.5在控制expression式上执行整数提升。 在每种情况下,标签中的常量expression式被转换为控制expression式的升级types。 如果转换的值与提升的控制expression式匹配,则控件跳转到匹配的大小写标签后面的语句。 否则,如果有一个默认标签,则控件跳转到标签语句。 如果没有转换的case常量expression式匹配,并且没有默认标签,则不会执行开关体的任何部分。
case语句和默认语句可以在switch语句中以任何顺序出现。 默认子句是一个可选的子句,如果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 /默认),你的案例很长,所以你不想重复整个案例代码在底部为默认值
这在一些情况下是有效的和非常有用的。
考虑下面的代码:
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 }
重点是上面的代码比级联更可读,更高效。 你可以把最后的默认值,但它是毫无意义的,因为它会把注意力集中在错误情况,而不是正常情况下(这是default
情况)。
事实上,这并不是一个很好的例子,在调查中你知道最多可能发生多less事件。 我真正的观点是,在有“例外”和正常情况下,有一组定义好的input值的情况。 如果最好把例外或正常情况放在前面是一个select的问题。
在软件领域,我想到了另一个非常常见的情况:用一些terminal值recursion。 如果您可以使用开关来表示它,则default
将是包含recursion调用和区分元素(个案)terminal值的通常值。 通常不需要关注terminal价值。
另一个原因是这些案例的顺序可能会改变编译后的代码行为,这对于性能来说很重要。 大多数编译器会以与交换机中出现的代码相同的顺序生成编译的汇编代码。 这使得第一种情况与其他情况非常不同:除第一种情况外,所有情况都会涉及跳转,并且会清空处理器pipe道。 你可以理解它像分支预测器默认运行交换机中的第一个出现的情况。 如果一个案件比其他案件更普遍,那么你有很好的理由把它作为第一案件。
在阅读英特尔编译器分支循环重组代码优化之后,阅读这些评论是原始海报问这个问题的具体原因。
然后是代码可读性和代码性能之间的一些仲裁。 或许更好的向未来的读者发表评论为什么先出现一个案例。
是的,这是有效的,在某些情况下甚至是有用的。 一般来说,如果你不需要它,不要这样做。
在switch语句中没有定义的顺序。 你可以把这些案例看作是一个命名的标签,就像goto
标签一样。 与人们似乎认为在这里相反,在价值2的情况下,默认的标签不跳。 用一个经典的例子来说明,这里是达夫的装置 ,它是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); } }
一种情况是,我认为将一个“default”定位在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();
…做任何其他必要的事情来build立一个“安全”的条件
}
其他地方的代码然后可以检查(widget_state == WIDGET_INVALID_STATE),并提供任何错误报告或状态重置行为似乎是合适的。 例如,状态栏代码可能会显示一个错误图标,并且可以为WIDGET_INVALID_STATE和WIDGET_IDLE启用在大多数非空闲状态下禁用的“start widget”菜单选项。
用另一个例子来说明:如果“default”是一个意想不到的情况,并且你想logging这个错误,但是也做了一些合理的事情,这会很有用。 从我自己的一些代码示例:
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 }
有些情况下,当您将ENUM转换为string或将string转换为enum时,如果您正在向文件写入/读取文件。
您有时需要将其中一个值设为默认值,以涵盖手动编辑文件所产生的错误。
switch(textureMode) { case ModeTiled: default: // write to a file "tiled" break; case ModeStretched: // write to a file "stretched" break; }
“默认”条件可以在交换机内的任何地方存在一个case子句。 它不是最后一个条款。 我看到了代码,把默认作为第一个条款。 即使默认子句在上面,“case 2:”也会正常执行。
作为一个testing,我把示例代码放在一个名为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
这是有效的,但相当讨厌。 我会build议通常不好,因为它可能会导致一些非常混乱的意大利面代码。
将这些案例分解成几个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; } } }
@Tristopia:你的第二个例子可能是一个很好的用于后续的最干净的例子:
for(i=0; s[i]; i++) { switch(s[i]) { case '"': case '\'': case '\\': d[dlen++] = '\\'; /* fall through */ default: d[dlen++] = s[i]; } }
但个人来说,我会将评论识别分成它自己的function:
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]; }