一、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语言深度剖析》,《深入理解计算机系统》