switch与跳转表

本文详细介绍了C语言中的switch语句,包括其工作原理、使用规范以及与跳转表的关系。通过实例分析了switch在不加break时的执行行为,探讨了continue在switch中的限制。此外,文章还解释了跳转表这一数据结构在switch中的应用,展示了不同情况下生成的汇编代码,强调了合理安排case顺序以提高效率的重要性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、switch跳转语句

类似于多路开关,可以根据给定条件匹配到符合条件的语句。正式点来说,是根据一个整形数值来进行多路分支的一种结构。在处理具有多种可能结果时,这种语句非常有效。它不仅挺高了C代码的可读性和逻辑性,而且使用跳转表来作为底层实现基础。

switch基本框架为

int condition
switch(condition)
{
	case condition1:statements ;
	case condition2:statements;
	case condition3:statements;
	
    ....
	case conditionn:statements;
	default:
	default_statment;
}

下面我们来具体实现以下

1 #include<stdio.h>
  2 #include<stdlib.h>
  3 
  4 int main()
  5 {
  6   int i=0;
  7   switch(i)
  8   {
  9     case 0:printf("i am 1\n");
 10     case 1:printf("i am 2\n");
 11     case 2:printf("i am 3\n");
 12     default:printf("error\n");
 13   }
 14 }

我们看一下结果:

发现我们只是想匹配i=1这个项,但是后面的也被执行了,为啥?我先抛一砖-->与switch的实现有关系。如何解决什么问题呢?每次case结束后break

  1 #include<stdio.h>
  2 #include<stdlib.h>
  3 
  4 int main()
  5 {
  6   int i=0;
  7   switch(i)
  8   {
  9     case 0:printf("i am 1\n"); break;
 10     case 1:printf("i am 2\n"); break;
 11     case 2:printf("i am 3\n"); break;
 12     default:printf("error\n"); break;
 13   }
 14 }

提到break,我们不经意间会联想到它的兄弟:continue,continue和break都可以作用于循环,但是在switch里面,continur起作用吗?实践,我们继续修改上面的代码来寻求我们的答案

#include<stdio.h>
  2 #include<stdlib.h>
  3 
  4 int main()
  5 {
  6   int i=0;
  7   switch(i)
  8   {
  9     default:printf("error\n"); continue;
 10     case 0:printf("i am 1\n"); continue;
 11     case 1:printf("i am 2\n"); continue;
 12     case 2:printf("i am 3\n"); continue;
 13   }
 14 }
~    

看结果

编译报错:continue不在循环里面,因此,continue不能在switch里使用,当然还有一种情况switch在循环里面,continue就会跳过本次循环。break直接跳出循环。

现在解决了我们的当前case匹配结束后,后面的语句也被执行的问题。那么现在再问?default放在前面会被执行吗?

实验证明没有,也就是说当所有条件都不满足才会执行default。我们应该在每一个switch语句里面添加default子句,因为这样可以检测到任何非法值,否则,所有不匹配,程序还继续执行,并且不给予你提示,这样的程序很难改,不是吗?

二、switch和跳转表

我们前面说过switch不跟break的情况和跳转表的关系。下面我们将由switch去看跳转表这种数据结构

我们将第一次不匹配的代码转成汇编代码,为了让实验更加明显,我们添加多个case语句

.file   "swich_t.c"
  2     .section    .rodata
  3 .LC0:
  4     .string "i am 0"
  5 .LC1:
  6     .string "i am 1"
  7 .LC2:
  8     .string "i am 2"
  9 .LC3:
 10     .string "i am 3"
 11 .LC4:
 12     .string "i am 4"
 13 .LC5:
 14     .string "i am 5"
 15 .LC6:
 16     .string "i am 6"
 17 .LC7:
 18     .string "i am 7"
 19 .LC8:
 20     .string "i am 8"
 21 .LC9:
 22     .string "error"
 23     .text
 24 .globl main
 25     .type   main, @function
 26 main:
 27     pushl   %ebp  //将调用main函数的栈帧压栈
 28     movl    %esp, %ebp  
 29     andl    $-16, %esp
 30     subl    $32, %esp
 31     movl    $0, 28(%esp)
 32     cmpl    $8, 28(%esp)
 33     ja  .L2
 34     movl    28(%esp), %eax
 35     sall    $2, %eax
 36     movl    .L12(%eax), %eax
 37     jmp *%eax
 38     .section    .rodata
 39     .align 4   //4字节对齐
 40     .align 4
 41 .L12:
 42     .long   .L3
 43     .long   .L4
 44     .long   .L5
 45     .long   .L6
 46     .long   .L7
 47     .long   .L8
 48     .long   .L9
 49     .long   .L10
 50     .long   .L11
 51     .text
 52 .L3:
 53     movl    $.LC0, (%esp)
 54     call    puts
 55 .L4:
 movl    $.LC1, (%esp)
 57     call    puts
 58 .L5:
 59     movl    $.LC2, (%esp)
 60     call    puts
 61 .L6:
 62     movl    $.LC3, (%esp)
 63     call    puts
 64 .L7:
 65     movl    $.LC4, (%esp)
 66     call    puts
 67 .L8:
 68     movl    $.LC5, (%esp)
 69     call    puts
 70 .L9:
 71     movl    $.LC6, (%esp)
 72     call    puts
 73 .L10:
 74     movl    $.LC7, (%esp)
 75     call    puts
 76 .L11:
 77     movl    $.LC8, (%esp)
 78     call    puts
 79 .L2:
 80     movl    $.LC9, (%esp)
 81     call    puts
 82     leave
                                                              82,2-5        93%
 ret
 84     .size   main, .-main
 85     .ident  "GCC: (GNU) 4.4.4 20100726 (Red Hat 4.4.4-13)"
 86     .section    .note.GNU-stack,"",@progbits

观察代码,我们发现一段有趣的段,那就是程序的第二个.rodata段,

.L12:
 42     .long   .L3
 43     .long   .L4
 44     .long   .L5
 45     .long   .L6
 46     .long   .L7
 47     .long   .L8
 48     .long   .L9
 49     .long   .L10
 50     .long   .L11
 51     .text

它就是我们所说的符号表,其中.L2给出表的基址,.Li(i=3...11)是表内偏移量,学过组成原理的朋友对这种基址变址寻址的方式会非常熟悉。

注意这段代码是在.text段内的,也就是说。main函数会按照表顺序逐个执行,因此打印了除匹配以后的数据。

在加入break之后的汇编代码如下

   .file   "swich_t.c"
  2     .section    .rodata
  3 .LC0:
  4     .string "i am 0"
  5 .LC1:
  6     .string "i am 1"
  7 .LC2:
  8     .string "i am 2"
  9 .LC3:
 10     .string "i am 3"
 11 .LC4:
 12     .string "i am 4"
 13 .LC5:
 14     .string "i am 5"
 15 .LC6:
 16     .string "i am 6"
 17 .LC7:
 18     .string "i am 7"
 19 .LC8:
 20     .string "i am 8"
 21 .LC9:
 22     .string "error"
 23     .text
 24 .globl main
 25     .type   main, @function
 26 main:
 27     pushl   %ebp
 28     movl    %esp, %ebp
 andl    $-16, %esp
 30     subl    $32, %esp
 31     movl    $0, 28(%esp)
 32     cmpl    $8, 28(%esp)
 33     ja  .L2
 34     movl    28(%esp), %eax
 35     sall    $2, %eax
 36     movl    .L12(%eax), %eax
 37     jmp *%eax
 38     .section    .rodata
 39     .align 4
 40     .align 4
 41 .L12:
 42     .long   .L3
 43     .long   .L4
 44     .long   .L5
 45     .long   .L6
 46     .long   .L7
 47     .long   .L8
 48     .long   .L9
 49     .long   .L10
 50     .long   .L11
 51     .text
 52 .L3:
 53     movl    $.LC0, (%esp)
 54     call    puts
 55     jmp .L15
 .L4:
 57     movl    $.LC1, (%esp)
 58     call    puts
 59     jmp .L15
 60 .L5:
 61     movl    $.LC2, (%esp)
 62     call    puts
 63     jmp .L15
 64 .L6:
 65     movl    $.LC3, (%esp)
 66     call    puts
 67     jmp .L15
 68 .L7:
 69     movl    $.LC4, (%esp)
 70     call    puts
 71     jmp .L15
 72 .L8:
 73     movl    $.LC5, (%esp)
 74     call    puts
 75     jmp .L15
 76 .L9:
 77     movl    $.LC6, (%esp)
 78     call    puts
 79     jmp .L15
 80 .L10:
 81     movl    $.LC7, (%esp)
 82     call    puts
    jmp .L15
 84 .L11:
 85     movl    $.LC8, (%esp)
 86     call    puts
 87     jmp .L15
 88 .L2:
 89     movl    $.LC9, (%esp)
 90     call    puts
 91 .L15:
 92     leave
 93     ret
 94     .size   main, .-main
 95     .ident  "GCC: (GNU) 4.4.4 20100726 (Red Hat 4.4.4-13)"
 96     .section    .note.GNU-stack,"",@progbits   jmp .L15
 80 .L10:
 81     movl    $.LC7, (%esp)
 82     call    puts
    jmp .L15
 84 .L11:
 85     movl    $.LC8, (%esp)
 86     call    puts
 87     jmp .L15
 88 .L2:
 89     movl    $.LC9, (%esp)
 90     call    puts
 91 .L15:
 92     leave
 93     ret
 94     .size   main, .-main
 95     .ident  "GCC: (GNU) 4.4.4 20100726 (Red Hat 4.4.4-13)"
 96     .section    .note.GNU-stack,"",@progbits

现在在加入break之后,在进入跳转表进行指定调用之后,都会都会使用jmp .L15直接到leave离开和返回指令,不会继续,粘着的执行。所以在加入break之后的swicth是在匹配成功之后多了一个控制转移,使得它正确的跳转到离开switch结构的地方。

接着,我们介绍一下跳转表的数据结构表现形式

跳转表是一个数组,表项i是一个代码段地址,这个代码段实现当开关索引与i相等时应该采取的动作。程序代码使用开关代码索引值来执行一个跳转表内数组的索引即

                                               F(i)=arrary_head_addr+i

arrary表示一个数据序列的首地址,i代表序列内部偏移量。给出首地址,按索引值找出数据来确定目标。和使用If-else相比较,switch少了一个预测分支的环节,一次对比,直接查表,提高了指令的执行速率。

我们可以自定义一个跳转表(做成hashtable更好),这里我们做成简单的单链表,对第i个元素取值就是获取链表中第i个元素,达到简单的匹配

typedef struct SkipTable_base
{
	int *head;
}

typedef struct Skip_Node
{
	int data
	struct Skip_Node* next;
}

三、swicth的使用规范

1.按照字母或者数字顺序排列case

2.将常用的放在前面,异常的放后面

3.按频率排序case

参考文档:《C语言深度剖析》,《深入理解计算机系统》

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值