C语言if、switch原理

条件语句是计算机编程语言中的三大流程之一,而在C语言中,if和switch是条件分支的重要组成部分。因此有必要弄明白它的底层原理

if原理

if的功能是计算判断条件的值,根据返回的值的不同来决定跳转到哪个部分。值为真则跳转到if语句块中,否则跳过if语句块。下面来分析一个简单的if实例:

1
2
3
4
5
6
7
8
9
if(argc > 0)
{
printf("argc > 0\n");
}
if (argc <= 0)
{
printf("argc <= 0\n");
}
printf("argc = %d\n", argc);

它对应的汇编代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
9:        if(argc > 0)
cmp dword ptr [ebp+8],0
0040102C jle main+2Bh (0040103b) ;argc <= 0就跳转到下一个if处
10: {
11: printf("argc > 0\n");
0040102E push offset string "argc > 0\n" (0042003c)
call printf (00401090)
add esp,4
12: }
13: if (argc <= 0) ;argc > 0跳转到后面的printf语句输出argc的值
0040103B cmp dword ptr [ebp+8],0
0040103F jg main+3Eh (0040104e)
14: {
15: printf("argc <= 0\n");
push offset string "argc <= 0\n" (0042002c)
call printf (00401090)
0040104B add esp,4
16: }
17: printf("argc = %d\n", argc);
0040104E mov eax,dword ptr [ebp+8]
push eax
push offset string "argc = %d\n" (0042001c)
call printf (00401090)
0040105C add esp,8

根据汇编代码我们看到,首先执行第一个if中的比较,jle表示当cmp得到的结果≤0时会进行跳转,第二个if在汇编中的跳转条件是>0,从这个上面可以看出在代码执行过程当中if转换的条件判断语句与if的判断结果时相反的,也就是说cmp比较后不成立则跳转,成立则向下执行。同时每一次跳转都是到当前if语句的下一条语句。

下面来看看if…else…语句的跳转。

1
2
3
4
5
6
7
8
if(argc > 0)
{
printf("argc > 0\n");
}else
{
printf("argc <= 0\n");
}
printf("argc = %d\n", argc);

它所对应的汇编代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
00401028   cmp         dword ptr [ebp+8],0
0040102C jle main+2Dh (0040103d) ;条件不满足则跳转到else语句块中
10: {
11: printf("argc > 0\n");
0040102E push offset string "argc > 0\n" (0042003c)
00401033 call printf (00401090)
00401038 add esp,4
12: }else
0040103B jmp main+3Ah (0040104a);如果执行if语句块就会执行这条语句跳出else语句块
13: {
14: printf("argc <= 0\n");
0040103D push offset string "argc <= 0\n" (0042002c)
00401042 call printf (00401090)
00401047 add esp,4
15: }
16: printf("argc = %d\n", argc);
0040104A mov eax,dword ptr [ebp+8]

上述的汇编代码指出,对于if…else..语句,首先进行条件判断,if表达式为真,则继续执行if快中的语句,然后利用jmp跳转到else语句块外,否则会利用jmp跳转到else语句块中,然后依次执行其后的每一句代码。
最后再来展示if…else if…else这种分支结构:

1
2
3
4
5
6
7
8
9
10
11
if(argc > 0)
{
printf("argc > 0\n");
}else if(argc < 0)
{
printf("argc < 0\n");
}else
{
printf("argc == 0\n");
}
printf("argc = %d\n", argc);

汇编代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
9:        if(argc > 0)
00401028 cmp dword ptr [ebp+8],0
0040102C jle main+2Dh (0040103d);条件不满足则会跳转到下一句else if中
10: {
11: printf("argc > 0\n");
0040102E push offset string "argc > 0\n" (00420f9c)
00401033 call printf (00401090)
00401038 add esp,4
12: }else if(argc < 0)
0040103B jmp main+4Fh (0040105f) ;当上述条件符合则执行这条语句跳出分支外,跳转的地址正是else语句外的printf语句
0040103D cmp dword ptr [ebp+8],0
00401041 jge main+42h (00401052)
13: {
14: printf("argc < 0\n");
00401043 push offset string "argc < 0\n" (0042003c)
00401048 call printf (00401090)
0040104D add esp,4
15: }else
00401050 jmp main+4Fh (0040105f)
16: {
17: printf("argc == 0\n");
00401052 push offset string "argc <= 0\n" (0042002c)
00401057 call printf (00401090)
0040105C add esp,4
18: }
19: printf("argc = %d\n", argc);
0040105F mov eax,dword ptr [ebp+8]

通过汇编代码可以看到对于这种结构,会依次判断每个if语句中的条件,当有一个满足,执行完对应语句块中的代码后,会直接调转到分支结构外部,当前面的条件都不满足则会执行else语句块中的内容。这个逻辑结构在某些情况下可以利用if return if return 这种结构来替代。当某一条件满足时执行完对应的语句后直接返回而不执行其后的代码。一条提升效率的做法是将最有可能满足的条件放在前面进行比较,这样可以减少比较次数,提升效率。

switch原理

switch是另一种比较常用的多分支结构,在使用上比较简单,效率上也比if…else if…else高,下面将分析switch结构的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
switch(argc)
{
  case 1:
    printf("argc = 1\n");
    break;
  case 2:
    printf("argc = 2\n");
    break;
   case 3:
    printf("argc = 3\n");
    break;
   case 4:
    printf("argc = 4\n");
    break;
   case 5:
    printf("argc = 5\n");
    break;
   case 6:
    printf("argc = 6\n");
    break;
   default:
    printf("else\n");
    break;
}

对应的汇编代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
0040B798   mov         eax,dword ptr [ebp+8] ;eax = argc
0040B79B mov dword ptr [ebp-4],eax
0040B79E mov ecx,dword ptr [ebp-4] ;ecx = eax
0040B7A1 sub ecx,1
0040B7A4 mov dword ptr [ebp-4],ecx
0040B7A7 cmp dword ptr [ebp-4],5
0040B7AB ja $L544+0Fh (0040b811) ;argc 》 5则跳转到default处,至于为什么是5而不是6,看后面的说明
0040B7AD mov edx,dword ptr [ebp-4] ;edx = argc
0040B7B0 jmp dword ptr [edx*4+40B831h]
11: case 1:
12: printf("argc = 1\n");
0040B7B7 push offset string "argc = 1\n" (00420fc0)
0040B7BC call printf (00401090)
0040B7C1 add esp,4
13: break;
0040B7C4 jmp $L544+1Ch (0040b81e)
14: case 2:
15: printf("argc = 2\n");
0040B7C6 push offset string "argc = 2\n" (00420fb4)
0040B7CB call printf (00401090)
0040B7D0 add esp,4
16: break;
0040B7D3 jmp $L544+1Ch (0040b81e)
17: case 3:
18: printf("argc = 3\n");
0040B7D5 push offset string "argc = 3\n" (00420fa8)
0040B7DA call printf (00401090)
0040B7DF add esp,4
19: break;
0040B7E2 jmp $L544+1Ch (0040b81e)
20: case 4:
21: printf("argc = 4\n");
0040B7E4 push offset string "argc = 4\n" (00420f9c)
0040B7E9 call printf (00401090)
0040B7EE add esp,4
22: break;
0040B7F1 jmp $L544+1Ch (0040b81e)
23: case 5:
24: printf("argc = 5\n");
0040B7F3 push offset string "argc < 0\n" (0042003c)
0040B7F8 call printf (00401090)
0040B7FD add esp,4
25: break;
0040B800 jmp $L544+1Ch (0040b81e)
26: case 6:
27: printf("argc = 6\n");
0040B802 push offset string "argc <= 0\n" (0042002c)
0040B807 call printf (00401090)
0040B80C add esp,4
28: break;
0040B80F jmp $L544+1Ch (0040b81e)
29: default:
30: printf("else\n");
0040B811 push offset string "argc = %d\n" (0042001c)
0040B816 call printf (00401090)
0040B81B add esp,4
31: break;
32: }
33:
34: return 0;
0040B81E xor eax,eax

上面的代码中并没有看到像if那样,对每一个条件都进行比较,其中有一句话 “jmp dword ptr [edx*4+40B831h]” 这句话从表面上看应该是取数组中的元素,再根据元素的值来进行跳转,而这个元素在数组中的位置与eax也就是与argc的值有关,下面我们跟踪到数组中查看数组的元素值:
0040B831   B7 B7 40 00         
0040B835   C6 B7 40 00
0040B839   D5 B7 40 00
0040B83D   E4 B7 40 00
0040B841   F3 B7 40 00
0040B845   02 B8 40 00
通过对比可以发现0x0040b7b7是case 1处的地址,后面的分别是case 2、case 3、case 4、case 5、case 6处的地址,每个case中的break语句都翻译为了同一句话“jmp $L544+1Ch (0040b81e)”,所以从这可以看出,在switch中,编译器多增加了一个数组用于存储每个case对应的地址,根据switch中传入的整数在数组中查到到对应的地址,直接通过这个地址跳转到对应的位置,减少了比较操作,提升了效率。编译器在处理switch时会首先校验不满足所有case的情况,当这种情况发生时代码调转到default或者switch语句块之外。然后将传入的整数值减一(数组元素是从0开始计数)。最后根据参数值找到应该跳转的位置。
上述的代码case是从0~6依次递增,这样做确实可行,但是当我们在case中的值并不是依次递增的话会怎样?此时根据不同的情况编译器会做不同的处理。
1)一般仍然会建立这样的一个表,将case中出现的值填写对应的跳转地址,没有出现的则将这个地址值填入default对应的地址或者switch语句结束的地址,比如当我们上述的代码去掉case 5, 这个时候填入的地址值如下图所示:

2)如果每两个case之间的差距大于6,或者case语句数小于4则不会采取这种做法,如果再采用这种方式,那么会造成较大的资源消耗。这个时候编译器会采用索引表的方式来进行地址的跳转。
下面有这样一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
switch(argc)
{
case 1:
printf("argc = 1\n");
break;
case 2:
printf("argc = 2\n");
break;
case 5:
printf("argc = 5\n");
break;
case 6:
printf("argc = 6\n");
break;
case 255:
printf("argc = 255\n");
default:
printf("else\n");
break;
}

它对应的汇编代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
0040B798   mov         eax,dword ptr [ebp+8]
0040B79B mov dword ptr [ebp-4],eax
0040B79E mov ecx,dword ptr [ebp-4] ;到此eax = ecx = argc
0040B7A1 sub ecx,1
0040B7A4 mov dword ptr [ebp-4],ecx
0040B7A7 cmp dword ptr [ebp-4],0FEh
0040B7AE ja $L542+0Dh (0040b80b) ;当argc > 255则跳转到default
0040B7B0 mov eax,dword ptr [ebp-4]
0040B7B3 xor edx,edx
0040B7B5 mov dl,byte ptr (0040b843)[eax]
0040B7BB jmp dword ptr [edx*4+40B82Bh]
11: case 1:
12: printf("argc = 1\n");
0040B7C2 push offset string "argc = 1\n" (00420fb4)
0040B7C7 call printf (00401090)
0040B7CC add esp,4
13: break;
0040B7CF jmp $L542+1Ah (0040b818)
14: case 2:
15: printf("argc = 2\n");
0040B7D1 push offset string "argc = 3\n" (00420fa8)
0040B7D6 call printf (00401090)
0040B7DB add esp,4
16: break;
0040B7DE jmp $L542+1Ah (0040b818)
17: case 5:
18: printf("argc = 5\n");
0040B7E0 push offset string "argc = 5\n" (00420f9c)
0040B7E5 call printf (00401090)
0040B7EA add esp,4
19: break;
0040B7ED jmp $L542+1Ah (0040b818)
20: case 6:
21: printf("argc = 6\n");
0040B7EF push offset string "argc < 0\n" (0042003c)
0040B7F4 call printf (00401090)
0040B7F9 add esp,4
22: break;
0040B7FC jmp $L542+1Ah (0040b818)
23: case 255:
24: printf("argc = 255\n");
0040B7FE push offset string "argc <= 0\n" (0042002c)
0040B803 call printf (00401090)
0040B808 add esp,4
25: default:
26: printf("else\n");
0040B80B push offset string "argc = %d\n" (0042001c)
0040B810 call printf (00401090)
0040B815 add esp,4
27: break;
28: }
29:
30: return 0;
0040B818 xor eax,eax

这段代码与上述的线性表相比较区别并不大,只是多了一句 “mov dl,byte ptr (0040b843)[eax]” 这似乎又是一个数组,通过查看内存可以知道这个数组的值分别为:00 01 05 05 02 03 05 05 … 04,下一句根据这些值在另外一个数组中查找数据,我们列出另外一个数组的值:
C2 B7 40 00   D1 B7 40 00  E0 B7 40 00  EF B7 40 00  FE B7 40 00  0B B8 40 00
通过对比我们发现,这些值分别是每个case与default入口处的地址,编译器先查找到每个值在数组中对应的元素位置,然后根据这个位置值再在地址表中从、找到地址进行跳转,这个过程可以用下面的图来表示:

这样通过一个每个元素占一个字节的表,来表示对应的case在地址表中所对应的位置,从而跳转到对应的地址,这样通过对每个case增加一个字节的内存消耗来达到,减少地址表对应的内存消耗。
在上述的汇编代码中,是利用dl寄存器来存储对应case在地址表中项,这样就会产生一个问题,当case 值大于 255,也就是超出了一个字节的,超出了dl寄存器的表示范围时,又该如何来进行跳转这个时候编译器会采用判定树的方式来进行判定,在根节点保存的是所有case值的中位数, 左子树都是大于这个大于这个值的数,右字数是小于这个值的数,通过每次的比较来得到正确的地址。比如下面的这个判定树:

首先与10进行比较,根据与10 的大小关系进入左子树或者右子树,再看看左右子树的分支是否不大于3,若不大于3则直接转化为对应的if…else if… else结构,大于3则检测分支是否满足上述的优化条件,满足则进行对应的地址表或者索引表的优化,否则会再次对子树进行优化,以便减少比较次数。