【问题标题】:how cases get evaluated in switch statements (C)如何在 switch 语句中评估案例 (C)
【发布时间】:2023-10-27 11:59:01
【问题描述】:

我正在学习 C,并且我已经熟悉基本的编程概念 我对 switch 语句有疑问 在下面的代码中为ex

for(int i =0 ; i<20; i++){

        switch(i){
        case 0: i+=5; /*label 1*/
        case 1: i+=2; /*label 2*/
        case 5: i+=5; /*label 3*/
        default : i+=4; /*label 4*/
        }
        printf("%d\t",i);
    }

输出是 16 21

也就是说先执行label 1处的case,然后由于没有break,label 2, 3, 4也会被执行,问题是如果label 1被执行那么i的值会更新为5,是否其他情况首先检查条件(如果 i =1 或 5 )然后执行,或者它只是执行任何事情而不检查?

【问题讨论】:

  • 我的问题是关于案例的评估,我想知道(在没有中断的情况下)标签为 2 的案例是否会在执行前检查 i 是否等于 1,或者它会在不检查的情况下执行
  • 基本上switch/case 的工作方式是这样的:对 switch 语句中的表达式求值,然后如果没有一个 case 标签与表达方式。就这样。案例标签只是标签,在switch 之外的其他地方不进行任何检查。
  • 有道理,谢谢大家
  • 谢谢,对于任何阅读本文的人,这些链接对我来说很有用,我建议你看看

标签: c switch-statement break


【解决方案1】:

这是一个非常好的问题,实际上揭示了 C 和 C++ 中 switch 语句的内部结构,有时会与级联 if-else 语句混淆。

C/C++ 中的switch 语句的工作原理如下:

  • (1) 首先,它评估在switch 语句中作为条件呈现的表达式
  • (2) 将结果存储在堆栈中或使用通用寄存器
  • (3) 使用该结果,它会尝试通过使用跳转表(当可以构建一个跳转表时)以尽可能少的比较跳转到相应的 case 语句。

由于 (1) 和 (2),您创建的 switch 的行为与您预期的不同,并且它不会在执行 case 语句期间重新评估初始表达式。

与级联 if-else 语句相比,case 语句本质上是按顺序编译的指令块,由 (3) 中提到的跳转表引用。一旦执行到达case 语句,如果没有遇到break,它将自动级联到下一个case 语句。 break 实际上指示编译器跳过 switch 语句并停止执行case 语句。

查看您的 switch 语句的注释反汇编,以便更好地了解幕后发生的事情:

   0x56555585 <+56>:    mov    -0x10(%ebp),%eax       ;<--- store "i" (the switch condition) into EAX
   0x56555588 <+59>:    cmp    $0x1,%eax              ;<--- check "case 1"
   0x5655558b <+62>:    je     0x5655559a <main+77>   ;<--- jump if equal to "case 1"
   0x5655558d <+64>:    cmp    $0x5,%eax              ;<--- check "case 5"
   0x56555590 <+67>:    je     0x5655559e <main+81>   ;<--- jump if equal to "case 5" 
   0x56555592 <+69>:    test   %eax,%eax              ;<--- check "case 0"
   0x56555594 <+71>:    jne    0x565555a2 <main+85>   ;<--- jump if not equal to "default"
   0x56555596 <+73>:    addl   $0x5,-0x10(%ebp)       ;<--- case 0
   0x5655559a <+77>:    addl   $0x2,-0x10(%ebp)       ;<--- case 1
   0x5655559e <+81>:    addl   $0x5,-0x10(%ebp)       ;<--- case 5
   0x565555a2 <+85>:    addl   $0x4,-0x10(%ebp)       ;<--- default

注意:这是使用-m32 -O0 gcc 选项构建的,以使用更易于阅读的 32 位代码,并禁用优化。

您可以清楚地看到,在进行跳转后(对任何 case 语句),没有进一步重新评估 i (-0x10(%ebp))。另外,当 case 执行时,如果没有使用break,它会自动级联到下一个。

现在,您可能会问自己为什么会出现这种奇怪的行为,答案在 (3):以尽可能少的比较跳到相应的 case 语句

case 语句的数量真正增加时,特别是当用于case 语句的值之间的分布是恒定的时,C/C++ 中的switch 语句显示了它们的真正实力。

例如,假设我们有一个大的 switch 语句,其中包含 100 个 case 值,1 在 case 值之间具有恒定分布,并且 switch 表达式 (i) 的计算结果为 100 (最后caseswitch):

switch (i) {
 case 1: /*code for case 1*/ break;
 case 2: /*code for case 2*/ break;
 [...]
 case 99: /*code for case 99*/ break;
 case 100: /*code for case 100*/ break;
}

如果您使用级联 if-else 语句,您将获得 100 次比较,但此 switch 只需使用几条指令即可获得相同的结果,顺序如下:

  • 首先:编译器将索引跳转表中的所有case 语句
  • second:它将评估switch中的条件并存储结果(即:获取i
  • 第三个:根据结果计算跳转表中对应的索引(即:将i1,第一个case语句,结果索引为99)
  • 第四,不做任何操作直接跳转到对应的case

如果您的案例值的分布范围为2,则同样适用:

switch (i) {
 case 1: /*code for case 1*/ break;
 case 3: /*code for case 3*/ break;
 [...]
 case 99: /*code for case 99*/ break;
 case 101: /*code for case 101*/ break;
}

您的编译器也应该检测到这种传播,并且在减去第一个 case 值(即 1)后将除以 2 以获得跳转表的相同索引。

switch 语句的这种复杂的内部工作使其成为 C/C++ 中非常强大的工具,当您想要基于只能在运行时评估的值分支代码时,并且该值属于一个均匀分布的集合,或者至少是均匀分布的一组值。

case 值没有均匀分布时,switch 的效率会降低,并且它开始执行类似于我们使用级联 if-else 代替的情况。

【讨论】:

  • 你的回答没有问题要问是否有任何资料或书籍可以详细解释?
【解决方案2】:

在 switch 语句的标记部分中没有 break 语句

for(int i =0 ; i<20; i++){

    switch(i){
    case 0: i+=5; /*label 1*/
    case 1: i+=2; /*label 2*/
    case 5: i+=5; /*label 3*/
    default : i+=4; /*label 4*/
    }
    printf("%d\t",i);
} 

所以当i 等于0(在循环的第一次迭代中)时,所有这些语句

    case 0: i+=5; /*label 1*/
    case 1: i+=2; /*label 2*/
    case 5: i+=5; /*label 3*/
    default : i+=4; /*label 4*/

在将控制权传递给 case 标签0: 后顺序执行,i 变为等于16

你可以想象循环的第一次迭代中的 switch 语句如下方式

goto Label0;

Label0: i+=5; /*label 1*/
Label1: i+=2; /*label 2*/
Label5: i+=5; /*label 3*/
Labeldefault : i+=4; /*label 4*/

标签只是标签。他们不执行任何代码。在对 switch 语句中的表达式求值后

switch(i)

控制权交给对应的带标签语句,如果没有跳转语句,则依次执行后面的所有语句。

然后在 for 循环的第三个表达式中,i 递增并等于 17

因此,在 for 循环的下一次迭代中,控制立即传递给标签 default 并且 i 变为等于 21

【讨论】:

  • 我知道当 i = 0 标签 1 被执行时,这使得 i = 5,之后所有其他情况也将被执行,而不检查 i = 1 或 5 吗?
  • @DJEBARRARABAHABDERRAZAK 查看我更新的帖子。表达式的检查仅在开关本身中完成。标签只是标签。它们不执行任何代码。
  • @DJEBARRARABAHABDERRAZAK 完全没有。不客气。:)