开关语句:必须默认为最后一种情况?

考虑下面的switch语句:

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

此代码编译,但它是有效的(=定义的行为)C90/C99?我从未见过代码的默认的情况不是最后一个情况。

< p > 编辑: < br > 正如Jon笼KillianDS所写的:这是非常丑陋和令人困惑的代码,我很清楚这一点。我只是对一般语法(它是定义的吗?)和预期的输出感兴趣
140537 次浏览

是的,这是有效的,在某些情况下甚至是有用的。一般来说,如果你不需要它,就不要做。

case语句和default语句可以在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/default),你的案例非常长,所以你不想在默认的底部重复整个案例代码,这非常有用

这是合理的,但相当令人讨厌。我的建议是,允许出现漏洞通常是不好的,因为它会导致一些非常混乱的意大利面条代码。

将这些情况分解成几个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:你的第二个例子可能是follow-through的一个很好的例子:

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

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

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

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

考虑下面的代码:

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将是包含递归调用和终端值的区分元素(个别情况)的通常值。通常不需要关注终端值。

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

阅读评论,这就是为什么最初的海报在阅读Intel编译器分支循环重组关于代码优化后提出这个问题的具体原因。

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

C99标准没有明确说明这一点,但综合所有事实来看,它是完全有效的。

casedefault标签等价于goto标签。参见6.8.1标记语句。特别有趣的是6.8.1.4,它启用了前面提到的达夫装置:

任何语句前都可以加a 声明标识符为的前缀 标签名称。标签本身 不改变流控制,其中

编辑:开关内的代码没有什么特别的;它是一个正常的代码块,就像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标签的表达式必须是一个整数常数 表达和没有两种的情况 常数表达式是一样的 开关语句也应相同 转换后的值。可能有 一个交换机最多只能有一个默认标签 声明。< / p >

所有的情况都被评估,然后它跳转到默认标签,如果给定:

6.8.4.2.5在控制上执行整数提升 表达式。中的常数表达式 每个case标签都转换为 控制的提升类型 表达式。如果一个转换后的值 与晋升人员相匹配 控制表达式,控制跳转 匹配后的语句 情况下标签。否则,如果有 默认标签,控件跳转到 标签声明。如果没有转换 大小写常量表达式匹配和 没有默认标签,没有部分

.执行开关体

在一种情况下,我认为将default情况放置在switch语句的末尾以外的其他地方是合适的,即在状态机中,无效状态应该重置机器并继续执行,就像它是初始状态一样。例如:

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,并提供任何合适的错误报告或状态重置行为。例如,状态条形码可以显示一个错误图标,而“start widget”;菜单选项,在大多数非空闲状态下被禁用,可以为WIDGET_INVALID_STATEWIDGET_IDLE启用。

另一个例子:如果“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
}

有些情况下,当您正在将ENUM转换为字符串或将字符串转换为ENUM时,您正在写入/读取文件或从文件中读取文件。

有时需要将其中一个值设为默认值,以弥补手动编辑文件时所犯的错误。

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


case ModeStretched:
// write to a file "stretched"
break;
}
我有一个有趣的例子,将default放在顶部可以节省程序空间。它是为Arduino Nano设计的,节省了8字节的闪存(RAM是一样的)。 供你参考,这两组代码是

#if 1 // toggle this 0 or 1
// 3138/265 bytes
uint8_t g_BuiltinLedGlowState = 0; // dropping '= 0' saves nothing
void AdvanceBuiltinLedGlow_3Ph(){
switch(++g_BuiltinLedGlowState){
default:
g_BuiltinLedGlowState = 0;
// drop through // break;
case 0: // bright
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, HIGH);
break;
case 1: // dim
pinMode(LED_BUILTIN, INPUT_PULLUP);
break;
case 2: // off
pinMode(LED_BUILTIN, INPUT);
break;
}
}
#elif 1
// 3146/265 bytes
uint8_t g_BuiltinLedGlowState = 0; // dropping '= 0' saves nothing
void AdvanceBuiltinLedGlow_3Ph(){
switch(++g_BuiltinLedGlowState){
case 1: // bright
pinMode(LED_BUILTIN, OUTPUT);
digitalWrite(LED_BUILTIN, HIGH);
break;
case 2: // dim
pinMode(LED_BUILTIN, INPUT_PULLUP);
break;
case 3: // off
pinMode(LED_BUILTIN, INPUT);
// drop through // break;
default:
g_BuiltinLedGlowState = 0;
break;
}
}
#endif


// the loop function runs over and over again forever
void loop() {
Serial.println(g_BuiltinLedGlowState, DEC);
AdvanceBuiltinLedGlow_3Ph();
delay(1000);
}

我不知道你是否可以称之为“丑”。当然,在你必须这样写的情况下,它是非常有用的:

    switch (cond) {
case default: Instruction1;
...
InstructionN;
case 1: FinalInstruction;
}

您可以避免编写两次代码