查看: 621|回复: 0
打印 上一主题 下一主题

for循环剖析

[复制链接] qrcode

26

主题

37

帖子

104

积分

注册会员

Rank: 2

积分
104
楼主
跳转到指定楼层
发表于 2015-10-25 06:42 PM | 只看该作者 回帖奖励 |倒序浏览 |阅读模式

for循环是我最喜欢的循环结构了,本来以为我对for循环已经很了解了,但在最近是使用之中不断的出问题,所以我又对for循环进行了一次比较深入的“研究”,研究结果使我大吃一惊,不得不感叹,C语言真的是高深莫测啊!
好了,感慨完了,让我们从头开始来聊一聊这个最熟悉但又最难以捉摸的for循环吧。
for循环是C语言中最基本的循环结构了,其典型应用是在已知循环次数时,进行的一系列循环操作。基本语法格式举例如下:
for(i=0;i<=100;i++)
括号中有三个表达式,其中第一个表达式用来给循环计数器赋初值,第二个表达式用来判断是否满足循环条件,第三个表达式用来改变循环计数器的值。
当然,这只是最基本的用法,这三个表达式并非只能针对同一个变量,甚至,并非每个都必须出现,这只是在循环体的不同位置进行运算的三个普通表达式而已。例如下面,计算n的阶乘,直到大于100为止:
a=1;
for(i=1;a<=100;i++) a*=i;
另外,每个表达式的位置也并非只能放置一个表达式,要知道,C语言中有一种逗号表达式,用逗号将多个表达式分隔开,在处理上,当做一个表达式来对待,最右边的表达式运算结果即为整个表达式的值。基于此理论,上面的阶乘运算可以改写为下面的格式:
for(a=1,i=1;a<=100;i++) a*=i;
这样写有什么好处呢?当然绝不仅仅是为了扮酷,在结构上,能够将一个整体运算紧密的结合在一起,可以最大限度的减小程序修改时遗漏等失误。Ctrl+C和Ctrl+V应该在写程序及修改程序中经常用到吧,如果写成这样,在复制过程中想丢掉语句都难。
第二个表达式可以写成逗号表达式的形式吗?当然也可以,不过我现在先不举例,待会会和一些注意事项一起说明。现在我们先把for循环的结构剖析一下,看看for循环究竟是怎样运行的。
首先是第一个表达式,这个表达式虽然是在for循环的循环表达式中出现的,但却不在循环体内,其实是循环体前面紧邻循环体的一个表达式,这也是上面两个写法效果相同的原因。毕竟它本身就是在循环体外面的,前一个程序只是光明正大的给写在了外面。为了证实这一点,我们来看一下for循环的汇编代码(不同编译器可能会有所不同,这里是在Keil4.1下编译的,编译器版本是armcc 4.0.0.728,选择的处理器是8962,因此编译出来的汇编指令是ARM CortexM3的指令)。
第一种写法:
     4: a=1; 
0x000001A8 2101      MOVS     r1,#0x01
     5: for(i=1;a<=100;i++) a*=i; 
0x000001AA 2201      MOVS     r2,#0x01
0x000001AC E001      B        0x000001B2
0x000001AE 4351      MULS     r1,r2,r1
0x000001B0 1C52      ADDS     r2,r2,#1
0x000001B2 2964      CMP      r1,#0x64
0x000001B4 DDFB      BLE      0x000001AE
     6: }
第二种写法:
     4: for(a=1,i=1;a<=100;i++) a*=i; 
0x000001A8 2101      MOVS     r1,#0x01
0x000001AA 2201      MOVS     r2,#0x01
0x000001AC E001      B        0x000001B2
0x000001AE 4351      MULS     r1,r2,r1
0x000001B0 1C52      ADDS     r2,r2,#1
0x000001B2 2964      CMP      r1,#0x64
0x000001B4 DDFB      BLE      0x000001AE
     5: }
可以看出,二者编译出来的汇编代码完全一样。在这里,B是跳转指令,在跳转指令下面是循环体。循环体代码如下:
0x000001AE 4351      MULS     r1,r2,r1
0x000001B0 1C52      ADDS     r2,r2,#1
0x000001B2 2964      CMP      r1,#0x64
0x000001B4 DDFB      BLE      0x000001AE
了解一些汇编的不难看出来(我也是在验证这个for循环时看了一点ARM汇编,我参考的文档是Cortex-M3™ Technical Reference Manual),BLE是条件跳转,根据条件跳转到前面的某一行语句上,循环的基本写法。
C语言中对for循环的执行过程描述如下:首先计算一次表达式1的值(参考格式:for(表达式1;表达式2;表达式3),再计算表达式2的值,如果表达式2的值为true,则执行一次循环体,如果表达式2的值为false,则退出循环体。没执行完一次循环体后,计算表达式3的值,然后再计算表达式2的值,并根据表达式2的值决定是否继续执行循环体。
在这里表达式2需要在两个位置计算,一是刚进入循环时判断第一次循环是否执行,另外则是在每次执行完循环时判断是否进行下一次循环,一个位置是在整个循环体前,另一个位置是在表达式3之后。在编译过程中,为了减小程序体积,表达式2只在表达式3之后计算。同时在循环体前增加一个无条件跳转指令跳过整个循环体(包括表达式3)跳转到循环的结尾来做第一次判断。所以,for循环的执行是从最后的指令开始执行的。
总结一下,for循环中的三个表达式执行方式如下:表达式1在整个循环体的前面(循环体外)执行一次,表达式2为循环体的最后一组指令(可以有多个表达式组成,下同),表达式3为倒数第二组指令,在表达式2前面紧邻表达式执行。
注:关于循环体的定义,在C语言表达式中,for循环的三个表达式都不属于循环体,但是假如以汇编的跳转指令界定的话,表达式2和表达式3也应该属于循环体,毕竟它们也是以同样的次数循环执行的。以上只是为了说明我在文中使用的循环体名称,很久不玩汇编了,对一些定义也都忘记了,所以也可能官方定义不同。

前面我讲了for循环的执行过程及表达式1的用法,下面我再聊聊表达式2,这是for循环里最需要注意的表达式,非常规应用中一不小心就会出错。我们仍以一个例子来说明,如下,计算10的阶乘。
前面我们将一个阶乘放到了同一行语句中,可谓简洁极致了,不过对简洁的追求是永无止境的,下面将要把这个for循环体中唯一的语句也给移到括号中,即循环表达式中。如下:
for(a=1,i=1;a*=i,i<=10;i++) ;
这里的表达式1和表达式2均为逗号表达式,其中包含了两个表达式。由于逗号表达式的结果为最右边的表达式结果,所以决定循环执行的条件仍然是i<=10。
编译,成功,运行,在i=11时成功退出循环这个程序正在向着“酷”的方向发展。最后最关键的时刻到了,看运算结果。如果运算结果不对,再酷的程序也没用!
10的阶乘为3628800,程序的运行结果——39916800。酷“毙”了。
这个程序究竟是怎么给毙掉的,我们来单步调试一下。i=1,没问题;i=2,没问题;3、4、……;i=10,没问题;i=11,该退出了,怎么又计算了一下?
要知道,表达式2在for循环中是作为一个整体表达式来参加运算的,如果采用了逗号表达式,则里面的所有表达式都会运行相同的次数。在i=11的时候还需要判断才能退出循环,所以这个a*=i也会跟着计算一次。所以,这样用的时候要特别注意每一个表达式的运算次数。
再来一个跟表达式2相关的例子。程序跳转的条件是表达式2的运算结果,并不要求表达式2一定是逻辑表达式,这又给喜欢装酷的人提供了机会,把表达式3阉掉!如下:

for(i=10;i>0;i--)……
写成
for(i=10;i--;) ……
酷吧,反正i都是要减1的在哪儿减不一样啊。进入循环,判断i的值,不为0,执行循环体,然后减1,一直到i为0的时候终止循环。i--是先运算完毕再减一,跟我们的要求一致。
运行一下,偷工减料了,比我们设定的循环少了一次。少在哪儿了呢?再回忆一下for循环的运行过程吧:
进入循环,第一步先判断是否满足跳转条件。在哪儿判断的呢?汇编已经告诉我们了,在循环体的结尾,进入循环时会有一个无条件跳转指令跳转到循环体的结尾去运算表达式2的值。
看到了没,答案出来了。程序一开始会跳转去直接执行表达式2,而表达式2在表达式3之后,这就意味着一开始会忽略表达式3一次。而改写的写法将表达式3写在表达式2的位置,则程序不会忽略表达式3(其实这里应该称为“表达式2”,下同)的第一次运算,表达式3多运算了一次,循环自然少了一次!

回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

快速回复 返回顶部 返回列表