这是一个非常好的问题,实际上揭示了 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 (最后case 在switch):
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)
- 第三个:根据结果计算跳转表中对应的索引(即:将
i减1,第一个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 代替的情况。