45、深入理解 switch/case 语句

深入理解 switch/case 语句

1. switch/case 语句概述

switch(或 case)高级控制语句是高级编程语言(HLL)中的一种条件语句。与 if 语句不同,if 语句测试布尔表达式,并根据表达式的结果执行代码中的两条不同路径之一;而 switch/case 语句可以根据序数(整数)表达式的结果分支到代码中的几个不同点。

以下是 C/C++、Pascal 和 HLA 中 switch 和 case 语句的示例:
- C/C++ 的 switch 语句

switch( expression )
{
  case 0:
    << statements to execute if the 
        expression evaluates to zero >>
    break;
  case 1:
    << statements to execute if the 
        expression evaluates to one >>
    break;
  case 2:
    << statements to execute if the 
       expression evaluates to two >>
    break;
  <<etc>>
  default:
    << statements to execute if the expression is 
        not equal to any of these cases >>
}
  • Pascal 的 case 语句
case ( expression ) of
  0: begin
    << statements to execute if the 
        expression evaluates to zero >>
    end;
  1: begin
    << statements to execute if the 
        expression evaluates to one >>
    end;
  2: begin
    << statements to execute if the 
        expression evaluates to two >>
    end;
  <<etc>>
end; (* case *)
  • HLA 的 switch 语句
switch( REG32 )
  case( 0 )
    << statements to execute if 
        EAX contains zero >>
  case( 1 )
    << statements to execute 
        REG32 contains one >>
  case( 2 )
    << statements to execute if 
        REG32 contains two >>
  <<etc>>
  default
    << statements to execute if 
        REG32 is not equal to any of these cases >>
endswitch;

从这些示例可以看出,这些语句都具有相似的语法。

2. switch/case 语句的语义

在大多数编程入门课程和教材中,通常通过将 switch/case 语句与 if..else..if 语句链进行比较来讲解其语义。例如,下面的 Pascal 代码被认为与前面的 Pascal case 语句等效:

if( expression = 0 ) then begin
  << statements to execute if expression is zero >>
end
else if( expression = 1 ) then begin
  << statements to execute if expression is one >>
end
else if( expression = 2 ) then begin
  << statements to execute if expression is two >>
end;

虽然这个特定的序列将实现与 case 语句相同的结果,但 if..then..elseif 序列和 case 实现之间存在几个根本差异:
- 常量要求 :case 语句中的 case 标签必须都是常量,而在 if..then..elseif 链中,实际上可以将变量和其他非常量值与控制变量进行比较。
- 单一表达式限制 :switch/case 语句只能将单个表达式的值与一组常量进行比较,不能像 if..then..elseif 链那样,对一个 case 将一个表达式与一个常量进行比较,对另一个 case 将另一个表达式与第二个常量进行比较。

因此,if..then..elseif 链在语义上与 switch/case 语句不同,并且功能更强大。

3. 跳转表与链式比较

虽然 switch/case 语句比 if..then..elseif 链更具可读性和便利性,但这种语句最初被添加到高级编程语言中是为了提高效率,而不是为了可读性或便利性。

考虑一个有十个不同表达式要测试的 if..then..elseif 链。如果所有情况都是相互排斥且等可能的,那么平均而言,程序在遇到一个计算结果为 True 的表达式之前将执行五次比较。

在汇编语言中,可以通过使用表查找和间接跳转,在固定的时间内将控制转移到几个不同的位置,而与情况的数量无关。例如,以下是将 switch(i) 转换为汇编语言的简单实现:

// Conversion of 
//    switch(i)
//    { case 0:...case 1:...case 2:...case 3:...} 
// into assembly
static
  jmpTable: dword[4] := 
    { &label0, &label1, &label2, &label3 };
    // jmps to address specified by jmpTable[i]
    mov( i, eax );
    jmp( jmpTable[ eax*4 ] );  
label0:
    << code to execute if i = 0 >>
    jmp switchDone;
label1:
    << code to execute if i = 1 >>
    jmp switchDone;
label2:
    << code to execute if i = 2 >>
    jmp switchDone;
label3:
    << code to execute if i = 3 >>
switchDone:
  << Code that follows the switch statement >>

这个代码的执行过程如下:
1. jmpTable 声明定义了一个包含四个双字指针的数组,每个指针对应 switch 语句模拟中的一个 case。
2. 第一条机器指令将 switch 表达式(变量 i 的值)加载到 EAX 寄存器中。
3. 下一条指令(jmp)根据 EAX 作为索引从 jmpTable 数组中获取地址,并跳转到该地址。

与 if..then..elseif 实现相比,这种跳转表实现有以下优点和缺点:
- 优点 :当情况数量超过三四个时,跳转表实现通常比对应的 if..then..elseif 链更快,并且消耗的内存更少。因为无论情况数量如何,跳转表实现只需要两条机器指令(加上跳转表),而 if..then..elseif 实现的比较和条件分支指令数量会随着情况数量的增加而增加。
- 缺点
- 内存访问速度 :跳转表是内存中的一个数组,访问(非缓存)内存可能较慢,访问跳转表数组可能会影响系统性能。
- 连续值要求 :必须为最小 case 值和最大 case 值之间的每个可能值在表中都有一个条目,包括那些实际上没有提供显式 case 的值。例如,对于以下 Pascal case 语句:

case( i ) of
  0: begin
      << statements to execute if i = 0 >>
     end;
  1: begin
      << statements to execute if i = 1 >>
     end;
  5: begin
      << statements to execute if i = 5 >>
     end;
  8: begin
      << statements to execute if i = 8 >>
     end;
end; (* case *)

如果使用跳转表实现,需要九个条目来处理 0 到 8 的所有可能 case 值:

// Conversion of 
//    switch(i)
//    { case 0:...case 1:...case 5:...case 8:} 
// into assembly
static
  jmpTable: dword[9] := 
          { 
            &label0, &label1, &switchDone, 
            &switchDone, &switchDone, 
            &label5, &switchDone, &switchDone, 
            &label8
          };
    // jumps to address specified by jmpTable[i]
    mov( i, eax );
    jmp( jmpTable[ eax*4 ] );  
label0:
    << code to execute if i = 0 >>
    jmp switchDone;
label1:
    << code to execute if i = 1 >>
    jmp switchDone;
label5:
    << code to execute if i = 5 >>
    jmp switchDone;
label8:
    << code to execute if i = 8 >>
switchDone:
  << Code that follows the switch statement >>

为了处理任意范围的 case 值,可以对代码进行修改。例如,对于以下 switch 语句:

switch(i)
{ case 10:...case 11:...case 12:...case 15:...case 16:} 

转换为汇编语言如下:

// Conversion of 
//    switch(i)
//    { case 10:...case 11:...case 12:...case 15:...case 16:} 
// into assembly, that automatically handles values 
// greater than 16 and values less than 10.
static
  jmpTable: dword[7] := 
          { 
            &label10, &label11, &label12, 
            &switchDone, &switchDone, 
            &label15, &label16
          };
    // Check to see if the value is outside the
    //  range 10..16.
    mov( i, eax );
    cmp( eax, 10 );            
    jb switchDone;             
    cmp( eax, 16 );
    ja switchDone;
    // The "- 10*4" part of the following expression 
    // adjusts for the fact that EAX starts at 10 
    // rather than zero, we still need a zero-based
    // index into our array.
    jmp( jmpTable[ eax*4 - 10*4] );
switchDone:
  << Code that follows the switch statement >>

这里的主要区别在于:
- 比较 EAX 中的值是否在 10 到 16 的范围内,如果不在则跳转到 switchDone 标签。
- 修改了 jmpTable 索引为 [eax 4 – 10 4],以调整 EAX 从 10 开始而不是 0 的情况。

一个完全通用的 switch/case 语句实际上需要六条指令来实现:原来的两条指令加上四条用于测试范围的指令。再加上间接跳转的执行成本略高于条件分支,这就是 switch/case 语句(与 if..then..elseif 链相比)的盈亏平衡点大约在三到四个 case 的原因。

下面是一个简单的 mermaid 流程图,展示了跳转表实现的基本流程:

graph TD;
    A[开始] --> B[加载表达式值到 EAX];
    B --> C[检查范围];
    C -- 在范围内 --> D[根据 EAX 索引跳转表];
    C -- 不在范围内 --> E[跳转到默认];
    D --> F[执行对应代码];
    F --> G[结束];
    E --> G;

综上所述,跳转表实现虽然在效率上有优势,但也存在一些局限性,在使用时需要根据具体情况进行权衡。

4. switch/case 语句的其他实现方式

由于跳转表大小的问题,一些高级编程语言的编译器不会使用跳转表来实现 switch/case 语句,而是采用其他方式:
- 转换为 if..then..elseif 链 :一些编译器会简单地将 switch/case 语句转换为相应的 if..then..elseif 链。显然,当使用跳转表更合适时,这种编译器生成的代码在速度方面质量较低。
- 智能选择实现方式 :许多现代编译器在代码生成方面比较智能。它们会确定 switch/case 语句中的 case 数量以及 case 值的分布范围,然后根据一些阈值标准(代码大小与速度)选择跳转表或 if..then..elseif 实现方式,甚至可能结合多种技术。

例如,对于以下 Pascal case 语句:

case( i ) of
  0: begin
      << statements to execute if i = 0 >>
     end;
  1: begin
      << statements to execute if i = 1 >>
     end;
  2: begin
      << statements to execute if i = 2 >>
     end;
  3: begin
      << statements to execute if i = 3 >>
     end;
  4: begin
      << statements to execute if i = 4 >>
     end;
  1000: begin
      << statements to execute if i = 1000 >>
    end;
end; (* case *)

一个好的编译器会认识到大多数 case 在跳转表中表现良好,只有一个(或几个)case 是例外。它会将其转换为 if..then 和跳转表实现的组合指令序列,如下所示:

mov( i, eax );
cmp( eax, 4 );
ja try1000;
jmp( jmpTable[ eax*4 ] );

try1000:
    cmp( eax, 1000 );
    jne switchDone;
    << code to do if i = 1000 >>
switchDone:

此外,还有一些其他的实现方式:
- 生成二元搜索树 :许多现代优化编译器在有相当数量的 case 且跳转表会太大时,会生成二元搜索树来测试 case。例如,对于以下 C 程序:

#include <stdio.h>
extern void f( void );
int main( int argc, char **argv )
{
    int boolResult;
    switch( argc )
    {
        case 1:
            f();
            break;
        case 10:
            f();
            break;
        case 100:
            f();
            break;
        case 1000:
            f();
            break;
        case 10000:
            f();
            break;
        case 100000:
            f();
            break;
        case 1000000:
            f();
            break;
        case 10000000:
            f();
            break;
        case 100000000:
            f();
            break;
        case 1000000000:
            f();
            break;
    }
    return 0;
}

MSVC 编译器生成的 MASM 输出会对十个 case 进行二元搜索:

_main   PROC NEAR                   ; COMDAT
; File t.c
; Line 11
;
; Binary search. Is argc less or greater than 100,000?
    mov eax, DWORD PTR _argc$[esp-4]
    cmp eax, 100000             ; 000186a0H
    jg  SHORT $L1242
    je  SHORT $L1228
; Binary search: is argc less than 100 or
; greater than 100 (but less than 100,000)
    cmp eax, 100                ; 00000064H
    jg  SHORT $L1243
    je  SHORT $L1228
; Is argc == 1?
    dec eax
    je  SHORT $L1228
; is argc == 10? If not, branch to default
; case.
    sub eax, 9
    jne SHORT $L1225
; argc == 10 at this point.
;
; Line 49
    call    _f
; Line 53
    xor eax, eax
; Line 54
    ret 0
; Cases where argc is greater than 100
; but less than 100,000 (1,000 & 10,000)
$L1243:
; Line 11
    cmp eax, 1000
    je  SHORT $L1228
    cmp eax, 10000
    jne SHORT $L1225    ;Default case
; Line 49
    call    _f
; Line 53
    xor eax, eax
; Line 54
    ret 0
;Cases where argc is greater than 100,000
$L1242:
; Line 11
; Above or below 100,000,000?
    cmp eax, 100000000
    jg  SHORT $L1244
    je  SHORT $L1228    ;100,000,000
; Below 100,000,000 and above 10,000
    cmp eax, 1000000    ;1,000,000
    je  SHORT $L1228
    cmp eax, 10000000   ;10,000,000
    jne SHORT $L1225    ;Default case
; Line 49
    call    _f
; Line 53
    xor eax, eax
; Line 54
    ret 0
; Handle the case where it's 1,000,000,000
$L1244:
; Line 11
    cmp eax, 1000000000
    jne SHORT $L1225
$L1228:
; Line 49
    call    _f
; Default case and BREAK come down here:
$L1225:
; Line 53
    xor eax, eax
; Line 54
    ret 0
_main   ENDP
_TEXT   ENDS
END
  • 生成 2 - 元组表 :一些编译器,特别是用于某些微控制器设备的编译器,会生成 2 - 元组(记录/结构)表,其中元组的一个元素是 case 的值,第二个元素是如果值匹配时要跳转的地址。然后编译器会发出一个循环来扫描这个小表,查找当前 switch/case 表达式的值。如果是线性搜索,这种实现方式甚至比 if..then..elseif 链还慢;如果编译器发出二元搜索,那么代码可能比 if..then..elseif 链快(尽管可能不如跳转表实现快)。
  • 代码技巧实现 :有时,编译器会采用一些代码技巧在某些情况下生成稍好的代码。例如,对于以下 switch 语句:
switch( argc )
{
    case 1:
        f();
        break;
    case 2:
        g();
        break;
    case 10:
        h();
        break;
    case 11:
        f();
        break;
}

Microsoft Visual C++ 编译器生成的代码如下:

; File t.c
; Line 13
;
; Use ARGC as an index into the $L1240 table,
; which returns an offset into the $L1241 table:
    mov eax, DWORD PTR _argc$[esp-4]
    dec eax         ;--argc, 1 = 0, 2 = 1, 10 = 9, 11 = 10
    cmp eax, 10     ;Out of range of cases?
    ja  SHORT $L1229
    xor ecx, ecx
    mov cl, BYTE PTR $L1240[eax]
    jmp DWORD PTR $L1241[ecx*4]
    npad    3
$L1241:
    DD  $L1232  ;cases that call f
    DD  $L1233  ;cases that call g
    DD  $L1234  ;cases that call h
    DD  $L1229  ;Default case
$L1240:
    DB  0   ;case 1 calls f
    DB  1   ;case 2 calls g
    DB  3   ;default
    DB  3   ;default
    DB  3   ;default
    DB  3   ;default
    DB  3   ;default
    DB  3   ;default
    DB  3   ;default
    DB  2   ;case 10 calls h
    DB  0   ;case 11 calls f
; Here is the code for the various cases:
$L1233:
; Line 19
    call    _g
; Line 31
    xor eax, eax
; Line 32
    ret 0
$L1234:
; Line 23
    call    _h
; Line 31
    xor eax, eax
; Line 32
    ret 0
$L1232:
; Line 27
    call    _f
$L1229:
; Line 31
    xor eax, eax
; Line 32
    ret 0

这种代码通过先进行表查找将 argc 值映射到 0 到 3 的范围(对应不同的代码体和默认情况),虽然比跳转表短,但由于需要访问两个不同的内存表,速度稍慢。

5. 优化 switch/case 语句的建议

由于很少有编译器允许你明确指定如何翻译特定的 switch/case 语句,并且不同编译器的实现方式差异较大,因此在编写代码时需要考虑以下优化建议:
- 手动拆分 case :如果编译器对某个 switch 语句生成的跳转表过大,可以手动拆分 case。例如,对于有 case 0、1、2、3、4 和 1000 的 switch 语句,可以这样优化:
- Pascal 代码

if( i = 1000 ) then begin
  << statements to execute if i = 1000 >>
end
else begin
  case( i ) of
    0: begin
        << statements to execute if i = 0 >>
       end;
    1: begin
        << statements to execute if i = 1 >>
       end;
    2: begin
        << statements to execute if i = 2 >>
       end;
    3: begin
        << statements to execute if i = 3 >>
       end;
    4: begin
        << statements to execute if i = 4 >>
       end;
  end; (* case *)
end; (* if *)
- **C/C++ 代码**:
switch( i )
{
  case 0: 
        << statements to execute if i == 0 >>
        break;
  case 1: 
        << statements to execute if i == 1 >>
        break;
  case 2:
        << statements to execute if i == 2 >>
        break;
  case 3: 
        << statements to execute if i == 3 >>
        break;
  case 4:
        << statements to execute if i == 4 >>
       break;
  default:
    if( i == 1000 )
    {
      << statements to execute if i == 1000 >>
    }
    else
    {
      << Statements to execute if none of the cases match >>
    }
}
  • 根据概率选择实现方式 :虽然跳转表实现通常在有相当数量的 case 且每个 case 等可能时效率较高,但如果一两个 case 比其他 case 更有可能出现,使用 if..then..elseif 链(或 if..then..elseif 和 switch/case 语句的组合)可能更高效。例如,如果某个变量超过一半的时间值为 15,大约四分之一的时间值为 20,其余 25% 的时间为其他几个不同的值,那么使用 if..then..elseif 链来实现多路测试可能更合适。

下面是一个总结不同实现方式特点的表格:
| 实现方式 | 优点 | 缺点 | 适用场景 |
| ---- | ---- | ---- | ---- |
| 跳转表 | 当 case 数量较多时速度快,内存使用相对稳定 | 内存访问可能慢,需要连续值 | case 数量较多且值连续 |
| if..then..elseif 链 | 实现简单,可比较变量和非常量值 | 随着 case 数量增加速度变慢 | case 数量较少或值不连续 |
| 二元搜索树 | 对于大量分散的 case 查找速度较快 | 实现相对复杂 | case 数量多且分散 |
| 2 - 元组表 | 适用于微控制器设备 | 线性搜索慢 | 微控制器设备 |
| 代码技巧实现 | 代码较短 | 可能需要访问多个表,速度稍慢 | case 分布特殊 |

综上所述,在使用 switch/case 语句时,需要根据 case 数量、值的分布以及不同 case 的概率等因素综合考虑,选择合适的实现方式,以达到最佳的性能和代码可读性。

graph LR;
    A[switch/case 语句] --> B{编译器选择};
    B -- 跳转表合适 --> C[跳转表实现];
    B -- 跳转表不合适 --> D{情况};
    D -- case 少 --> E[if..then..elseif 链];
    D -- case 多且分散 --> F[二元搜索树];
    D -- 微控制器 --> G[2 - 元组表];
    D -- 特殊分布 --> H[代码技巧实现];
内容概要:本文档介绍了基于3D FDTD(时域有限差分)方法在MATLAB平台上对微带线馈电的矩形天线进行仿真分析的技术方案,重点在于模拟超MATLAB基于3D FDTD的微带线馈矩形天线分析[用于模拟超宽带脉冲通过线馈矩形天线的传播,以计算微带结构的回波损耗参数]宽带脉冲信号通过天线结构的传播过程,并计算微带结构的回波损耗参数(S11),以评估天线的匹配性能和辐射特性。该方法通过建立三维电磁场模型,精确求解麦克斯韦方程组,适用于高频电磁仿真,能够有效分析天线在宽频带内的响应特性。文档还提及该资源属于一个涵盖多个科研方向的综合性MATLAB仿真资源包,涉及通信、信号处理、电力系统、机器学习等多个领域。; 适合人群:具备电磁场与微波技术基础知识,熟悉MATLAB编程及数值仿真的高校研究生、科研人员及通信工程领域技术人员。; 使用场景及目标:① 掌握3D FDTD方法在天线仿真中的具体实现流程;② 分析微带天线的回波损耗特性,优化天线设计参数以提升宽带匹配性能;③ 学习复杂电磁问题的数值建模与仿真技巧,拓展在射频与无线通信领域的研究能力。; 阅读建议:建议读者结合电磁理论基础,仔细理解FDTD算法的离散化过程和边界条件设置,运行并调试提供的MATLAB代码,通过调整天线几何尺寸和材料参数观察回波损耗曲线的变化,从而深入掌握仿真原理与工程应用方法。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值