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

if(argc > )
{
printf("argc > 0\n");
}
if (argc <= )
{
printf("argc <= 0\n");
}
printf("argc = %d\n", argc);

它对应的汇编代码如下:

:        if(argc > )
cmp dword ptr [ebp+],
0040102C jle main+2Bh (0040103b) ;argc <= 0就跳转到下一个if处
: {
: printf("argc > 0\n");
0040102E push offset string "argc > 0\n" (0042003c)
call printf ()
add esp,
: }
: if (argc <= ) ;argc > 0跳转到后面的printf语句输出argc的值
0040103B cmp dword ptr [ebp+],
0040103F jg main+3Eh (0040104e)
: {
: printf("argc <= 0\n");
push offset string "argc <= 0\n" (0042002c)
call printf ()
0040104B add esp,
: }
: printf("argc = %d\n", argc);
0040104E mov eax,dword ptr [ebp+]
push eax
push offset string "argc = %d\n" (0042001c)
call printf ()
0040105C add esp,

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

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

if(argc > )
{
printf("argc > 0\n");
}else
{
printf("argc <= 0\n");
}
printf("argc = %d\n", argc);

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

   cmp         dword ptr [ebp+],
0040102C jle main+2Dh (0040103d) ;条件不满足则跳转到else语句块中
: {
: printf("argc > 0\n");
0040102E push offset string "argc > 0\n" (0042003c)
call printf ()
add esp,
: }else
0040103B jmp main+3Ah (0040104a);如果执行if语句块就会执行这条语句跳出else语句块
: {
: printf("argc <= 0\n");
0040103D push offset string "argc <= 0\n" (0042002c)
call printf ()
add esp,
: }
: printf("argc = %d\n", argc);
0040104A mov eax,dword ptr [ebp+]

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

最后再来展示if...else if...else这种分支结构:

if(argc > )
{
printf("argc > 0\n");
}else if(argc < )
{
printf("argc < 0\n");
}else
{
printf("argc == 0\n");
}
printf("argc = %d\n", argc);

汇编代码如下:

:        if(argc > )
cmp dword ptr [ebp+],
0040102C jle main+2Dh (0040103d);条件不满足则会跳转到下一句else if中
: {
: printf("argc > 0\n");
0040102E push offset string "argc > 0\n" (00420f9c)
call printf ()
add esp,
: }else if(argc < )
0040103B jmp main+4Fh (0040105f) ;当上述条件符合则执行这条语句跳出分支外,跳转的地址正是else语句外的printf语句
0040103D cmp dword ptr [ebp+],
jge main+42h ()
: {
: printf("argc < 0\n");
push offset string "argc < 0\n" (0042003c)
call printf ()
0040104D add esp,
: }else
jmp main+4Fh (0040105f)
: {
: printf("argc == 0\n");
push offset string "argc <= 0\n" (0042002c)
call printf ()
0040105C add esp,
: }
: printf("argc = %d\n", argc);
0040105F mov eax,dword ptr [ebp+]

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

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

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;
}

对应的汇编代码如下:

0040B798   mov         eax,dword ptr [ebp+] ;eax = argc
0040B79B mov dword ptr [ebp-],eax
0040B79E mov ecx,dword ptr [ebp-] ;ecx = eax
0040B7A1 sub ecx,
0040B7A4 mov dword ptr [ebp-],ecx
0040B7A7 cmp dword ptr [ebp-],
0040B7AB ja $L544+0Fh (0040b811) ;argc 》 5则跳转到default处,至于为什么是5而不是6,看后面的说明
0040B7AD mov edx,dword ptr [ebp-] ;edx = argc
0040B7B0 jmp dword ptr [edx*+40B831h]
: case :
: printf("argc = 1\n");
0040B7B7 push offset string "argc = 1\n" (00420fc0)
0040B7BC call printf ()
0040B7C1 add esp,
: break;
0040B7C4 jmp $L544+1Ch (0040b81e)
: case :
: printf("argc = 2\n");
0040B7C6 push offset string "argc = 2\n" (00420fb4)
0040B7CB call printf ()
0040B7D0 add esp,
: break;
0040B7D3 jmp $L544+1Ch (0040b81e)
: case :
: printf("argc = 3\n");
0040B7D5 push offset string "argc = 3\n" (00420fa8)
0040B7DA call printf ()
0040B7DF add esp,
: break;
0040B7E2 jmp $L544+1Ch (0040b81e)
: case :
: printf("argc = 4\n");
0040B7E4 push offset string "argc = 4\n" (00420f9c)
0040B7E9 call printf ()
0040B7EE add esp,
: break;
0040B7F1 jmp $L544+1Ch (0040b81e)
: case :
: printf("argc = 5\n");
0040B7F3 push offset string "argc < 0\n" (0042003c)
0040B7F8 call printf ()
0040B7FD add esp,
: break;
0040B800 jmp $L544+1Ch (0040b81e)
: case :
: printf("argc = 6\n");
0040B802 push offset string "argc <= 0\n" (0042002c)
0040B807 call printf ()
0040B80C add esp,
: break;
0040B80F jmp $L544+1Ch (0040b81e)
: default:
: printf("else\n");
0040B811 push offset string "argc = %d\n" (0042001c)
0040B816 call printf ()
0040B81B add esp,
: break;
: }
:
: return ;
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则不会采取这种做法,如果再采用这种方式,那么会造成较大的资源消耗。这个时候编译器会采用索引表的方式来进行地址的跳转。

下面有这样一个例子:

    switch(argc)
{
case :
printf("argc = 1\n");
break;
case :
printf("argc = 2\n");
break;
case :
printf("argc = 5\n");
break;
case :
printf("argc = 6\n");
break;
case :
printf("argc = 255\n");
default:
printf("else\n");
break;
}

它对应的汇编代码如下:

0040B798   mov         eax,dword ptr [ebp+]
0040B79B mov dword ptr [ebp-],eax
0040B79E mov ecx,dword ptr [ebp-] ;到此eax = ecx = argc
0040B7A1 sub ecx,1
0040B7A4 mov dword ptr [ebp-],ecx
0040B7A7 cmp dword ptr [ebp-],0FEh
0040B7AE ja $L542+0Dh (0040b80b) ;当argc > 255则跳转到default处
0040B7B0 mov eax,dword ptr [ebp-]
0040B7B3 xor edx,edx
0040B7B5 mov dl,byte ptr (0040b843)[eax]
0040B7BB jmp dword ptr [edx*+40B82Bh]
: case :
: printf("argc = 1\n");
0040B7C2 push offset string "argc = 1\n" (00420fb4)
0040B7C7 call printf ()
0040B7CC add esp,
: break;
0040B7CF jmp $L542+1Ah (0040b818)
: case :
: printf("argc = 2\n");
0040B7D1 push offset string "argc = 3\n" (00420fa8)
0040B7D6 call printf ()
0040B7DB add esp,
: break;
0040B7DE jmp $L542+1Ah (0040b818)
: case :
: printf("argc = 5\n");
0040B7E0 push offset string "argc = 5\n" (00420f9c)
0040B7E5 call printf ()
0040B7EA add esp,
: break;
0040B7ED jmp $L542+1Ah (0040b818)
: case :
: printf("argc = 6\n");
0040B7EF push offset string "argc < 0\n" (0042003c)
0040B7F4 call printf ()
0040B7F9 add esp,
: break;
0040B7FC jmp $L542+1Ah (0040b818)
: case :
: printf("argc = 255\n");
0040B7FE push offset string "argc <= 0\n" (0042002c)
0040B803 call printf ()
0040B808 add esp,
: default:
: printf("else\n");
0040B80B push offset string "argc = %d\n" (0042001c)
0040B810 call printf ()
0040B815 add esp,
: break;
: }
:
: return ;
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则检测分支是否满足上述的优化条件,满足则进行对应的地址表或者索引表的优化,否则会再次对子树进行优化,以便减少比较次数。

 

if和switch的原理的更多相关文章

  1. 知识扩展--if...else...与switch...case...的执行原理

    一.简述 编程语言中的条件分支结构有两种:if-else和switch-case,这两种条件分支之间可以相互转换,但是也存在一些区别,那么什么时候该用if-else,什么时候该用switch-case ...

  2. 一图详解java-class类文件原理

    摘要:徒手制作一张超大的类文件解析图,方便通过浏览这个图能马上回忆起class文件的结构以及内部的指令. 本文分享自华为云社区<[读书会第十二期]这可能是全网"最大".&qu ...

  3. 3.php基础(控制语句,函数,数组遍历)

    if条件判断语句 结构一:只判断true,不管false 结构二:既判断true,也判断false(二选一) 结构三:多条件判断 switch多分支结构 Switch语法结构说明: l Switch的 ...

  4. 网络基础和 TCP、IP 协议

    1.网络基本概念 1.1 什么是网络:一些网络设备按照一定的通讯规则(网络协议)进行通讯的系统. 1.2 VPN(虚拟私有网络)加密,相当于专线,从分支机构到总部. 1.3 资源共享的功能和特点: 数 ...

  5. Birkhoff-von Neumann Crossbar 光交换网络的调度方案

    Birkhoff-von Neumann Crossbar 光交换网络的调度方案 ​ This is a summary aimed at looking for "high perform ...

  6. 关于switch语句中使用String类型的实现原理

    在Java 7 以后,switch语句可以用作String类型上. 从本质来讲,switch对字符串的支持,其实也是int类型值的匹配.它的实现原理如下: 通过对case后面的String对象调用ha ...

  7. 详解JAVA字符串类型switch的底层原理

    基础 我们现在使用的Java的版本,基本上是都支持String类型的.当然除了String类型,还有int.char.byte.short.enum等等也都是支持的.然而在其底部实现中,还是基于 整型 ...

  8. 详解:Java字符串类型"switch"的底层原理

    前言: 最近更新得会比较频繁,希望大家见谅哦! 也感谢关注我的人,我会更加更加努力去做的! 基础 我们现在使用的Java的版本,基本上是都支持String类型的.当然除了String类型,还有int. ...

  9. java中Switch的实现原理浅谈

    switch的转换和具体系统实现有关,如果分支比较少,可能会转换为跳转指令(条件跳转指令和无条件跳转指令).但如果分支比较多,使用条件跳转会进行很多次的比较运算,效率比较低,可能会使用一种更为高效的方 ...

随机推荐

  1. KL25的AD采集操作

    飞思卡尔的KL25单片机AD做的是很不错的,SAR型能做到16位.不过数据手册就写得不怎么样了,简直可以说是坑爹,很难看懂.有的描述让人难以理解,你指望在别的地方对不理解的地方会有其他角度的描述,结果 ...

  2. java参数传递(值传递还是引用传递)

    Java中的参数传递机制一直以来大家都争论不休,究竟是“传值”还是“传址(传引用)”,争论的双方各执一词,互不相让.不但“菜鸟”们一头雾水,一些“老鸟”也只知道结果却说不出所以然来.我相信看过下面的内 ...

  3. 结构-行为-样式-angularJs笔记

    0.关于Ng-app   通过ngApp指令来引导Angularjs应用是一种简洁的方式 ,适合大多数情况.在高级开发中,例如使用脚本装载应用,您也可以使用Bootstrap手动引导AngularJs ...

  4. [ios2]发布时去除NSLog打印

    #if DEBUG #warning NSLogs will be shown #else #define NSLog(...) {} #endif

  5. 使用pip安装报错的处理方法_2

    终端更新pip出错 sudo pip install --upgrade pip : 1.ReadTimeoutError: HTTPSConnectionPool(host='pypi.Python ...

  6. Prefix the choice with ! to persist it to bower.json ? Answer (问你选择哪个1,2,3.........)

    Unable to find a suitable version for underscore, please choose one by typing on e of the numbers be ...

  7. linux开启telnet服务

    步骤: sudo apt-get install xinetd telnetd     安装成功后,系统会显示有相应得提示 sudo vim /etc/inetd.conf  并加入内容:  teln ...

  8. 修改html 属性,css样式。

    一 通过修改标签属性来改变它的样式  js设置和获取标签的属性 <script type="text/javascript"> window.onload = func ...

  9. ios在项目中打开word文档、ppt等总结

    最近在项目开发中遇到下载附件文档预览需求,在这里总结一下我的实现方法,本文最后会附带我写的demo下载地址 这里我总结了三种实现方法(1)用webView预览(2)通过UIDocumentIntera ...

  10. on事件绑定阻止冒泡的问题

    当使用on进行事件绑定时当要给document绑定click,而子元素要禁止冒泡,那么子元素里面的子元素的click事件就会无效了, 下面无效版: $('#queue').on('click', '. ...