C循环如何实现详解
开篇
几乎每种程序设计语言的语法中都会有语句的循环,跳转。像最为熟知的C语言便有 for 、 while 、 do---while 等等。这些循环一般都很容易理解和使用,对于程序中逻辑的实现也很有帮助。
只是很多人不曾知道,这些循环、跳转在计算机内部、在底层是如何实现的,于是在出现问题时还是没有好的解决办法,或者是虽然写出来程序,对于内部的逻辑,却还是隔了一层迷雾。
比如有人对这样一个问题:
for( i=0 ; i< 10 ; i++)
{
printf(”%i“,i);
}
for语句里面的 i++ 是什么时候执行的呢? 当循环开始时,是先执行括号里的 i++ 还是printf(”%i“,i)? 也就是说 ,第一个打印的数字是0 还是 1?
我相信这个问题很多人都遇到过,就是当for 循环结束后 i 的值到底是多少不是很确定,在这个问题上犯难,是在不值得。如果你也曾有过这种疑问,那么这篇文章就很值得继续看下去。
这篇文章将拨开迷雾,让你看到循环的本质。
这篇文章主要谈一下C中的这些循环, 跳转语句的内部实现机制。通过深入了解他们的内在,将让你在编程的时候逻辑更加清晰,出现问题的时候也更容易排查。
注:这篇文章中会涉及到一些基本的汇编知识,我们将通过分析 for 、 while 、 do---while 等的汇编表示形式,来弄清楚他们的实现机理。
if--else
if--else是基本的条件转移控制指令,也可以说是程序循环跳转实现的基础。
先看一个具体的例子,函数absdiff()比较两个参数x,y 的大小,返回他们差的绝对值。
我们创建了对于的C语言版本(a)、goto形式的版本(b)、 以及其对应的汇编形式(c):
创建goto版本是为了能更好的理解他的汇编形式。因为里面的goto语句很类似于汇编中的跳转语句。
(a)中 的C代码应该不用多做解释了吧,大家应该都能看懂。
(b)中的got0语句形式:第4 行是一个跳转语句,跳转到执行第8 行。也就是说当第3 行的条件满足的时候,跳转到第8 行执行。如果第3 行的条件不满足,则执行第 5 行的语句。 然后无条件跳转到代码的结尾:“ goto done;” 使用goto 语句通常被认为是一种不好的 编程风格,因为这样的语句通常难于调试和阅读,这里使用goto语句是为了构造出一种类似汇编实现的C语句。
(c)就是我们要讨论的重点了。其实条件跳转指令的汇编形式和他的goto形式有一定的相似之处。有语句跳转的时候都是这样的过程:先判断是否满足某个条件,如果满足,则跳到一个指定的的代码段继续执行程序,如果不满足,则执行另一条语句。
为了弄清楚它内部是如何实现的,现在我们来详细看一下他的汇编指令形式(c)
程序的 1 、2 行取得x y 的值。放在寄存器 edx eax 中,第三行比较x y 的大小(也就是比较edx eak 的大小)如果x > y 则跳到 .L2的地方执行(8 、9 行)执行x - y ,并将结果放在返回值中,然后程序顺序执行到程序结束处 .L3,反之,如果第三行判断结果是x<y ,则语句不会跳转,顺序执行y-x,并跳转到程序结束处 .L3.
现在,我们基本了解if --else 的执行机理了。主要是先判断条件值,从这个例子出发就是第 3 条语句: cmpl %eax,%edx。学过汇编的同学应该知道,cmpl是一条比较指令,他的执行会影响标志位。做个简单的假设: cmpl A,B 是比较A,B两个数的大小,比较结果存放在标志位 F 中,当A>B时,F为0,A<BF为1。 后面的jge .L是跳转语句,他的执行是和上一步比价的结果相关的,这里也就反映在标志位F 当中。当F 为1时(A<B),则跳转到 .L2执行 B-A。当比较结果为A>B 查看后标志位为0,则jge不跳转, 程序继续往下执行。
C中的if --else 的通用模板是这样的:
这里的 test-expr 是一个整数表达式,它的值为真(1、>0)则会执行第一个分支语句 , 否则执行第二个。但不管怎么样,都只会执行其中的一个。
对于这种模板,汇编的实现通常会用下面的形式:(这里用C语言描述)
汇编器会为 then-statement 和else-statement产生各自的代码快 ,通过条件跳转来执行相应的代码。
注意:程序并不是智能的,执行跳转的时候只会有两种判断(真和假),而且内部的跳转形式都是基于 go to 模式。所以说虽然goto 在编程中要少用,但在理解程序跳转的时候还是很重要的。我想因为goto 的实现方式和程序最终的实现方式如此的相似,才在C中引入goto 吧。
do---while
C中提供了多中循环结构,do-while while 和 for ,然而汇编中没有相应的指令存在。通过上面对 if -else 的分析, 已经知道了 C 中条件跳转在汇编中的基本实现方式(goto),现在要讨论的这些循环,在循环中都有一个条件判断的环节,当条件满足时,继续循环体, 如果条件不满足,则跳出循环体。和if-else 不同的在于这里的条件表达式可能会有改变,也就是每次判断的条件和上次不同。
只要理解了上面的if-else 的内容 ,其他的循环都能通过对if -else 的改进来实现。先给出这些循环的goto 形式,就能很快推测出其内部的实现方式。
通用形式:
do-while的通用形式如下:
do
body-staement
while(test-expr)
这个循环的效果就是反复执行 body-staement,对test-expr进行求值,如果求值结果不是0 则继续执行。可以看到,body-statement至少会被执行一次。
do-while 的通用形式可翻译为 如下所示的goto 语句:
loop:
body-statement
t=test-expr;
if(t)
goto loop;
对于上面的goto形式, 可以先想想汇编会是如何实现的:把loop里的代码放在一个单独的代码段里面(包含if 的判断跳转部分)程序顺序执行下来,第一次执行不用判断条件。执行玩body-statement后判断条件,如果满足,则继续这个循环。
好了,下面看一个具体的例子:
这是一个计算阶乘的函数实现,
(a)是c代码,
(c)是对应的汇编形式
(b)是汇编中寄存器和变量的映射关系
(a)中的代码比较简单就不在解释了。现在看(c)中的代码:
第 1 行是获得函数参数n的值,放在寄存器 edx 中,第 2 行设置 result =1 第 3 行的 L2 表明这是一个循环体。 (c) 中的第 4、 5 行实现了 (a)中 5、 6 行的功能。
(c)中的第6 行判断循环的条件,如果条件满足,则执行第 7 行 的跳转语句。
和do- while的通用形式所描述的一样,body-staement至少会被执行一次,这从汇编代码中也有体现,观察循环体 L2,在L2 没有关于L2 的跳转代码,也就是说在程序顺序往下执行的情况下,L2 至少会被执行一次,然后在 L2 内部判断是否继续执行循环体 L2。
while
whil循环和do-while类似,他的通用形式如下:
while(test-expr)
body-statement
和do-while不同的是,他先对test-expr求值,所以body-statement可能一次都不被执行。将while翻译为机器码有多种方法,其中一种常见的方式i就是把他翻译成do-while的形式。在第一次执行循环体之前加一个判断条件,如果条件满足,则进入do-while的循环体,如果不满足,直接跳过循环体。也就是上面说的:body-statement可能一次都不被执行。
下面先把while转换为do-while循环:
if(!test-expr)
goto done;
do
body-statement
while(test-expr);
done;
接下来可以吧这个do-while循环转换为goto语句:
t=test-expr;
if(!t)
goto done;
loop:
body-statement
t=test-expr;
if(t)
goto loop;
done
这个goto的版本和do-while的版本只是在循环体loop的前面加上了一个判断条件,如果条件满足, 就执行上面中的do-while循环体,如果不满足,直接跳过循环。
通过上面的goto版本,现在应该可以想一下while 的 汇编是如何实现了吧:
现在就可以猜测,while 的汇编只是在do-while汇编的基础上做出一些改动:在循环体loop之前加一个判断条件,如果条件成立,则执行loop,不成立则跳过loop。
下面还通过一个具体例子来说:
还是计算n 的阶乘,不过这次是用while实现
下面是C代码:
这很简单,就不做解释了,下面看他的goto形式:
和上面描述的一样,在3、4 行中加入了条件判断,如果条件不满足 第5行直接跳攻loop循环体,只有当条件满足时才进入循环体loop。再看loop中,7、8 行执行计算阶乘的必要操作,9、10行通过判断test-expr 决定是否继续循环。
好,下面来看while的汇编形式:
现在看汇编代码应该比较有经验了吧,这里的3、4 行执行条件判断,决定是否进入循环体.L10,相当于goto版本中的4、5 行,如果不满足,直接跳转到.L7的位置。
再看一下.L10 循环体:6、7 行执行阶乘的必要操作,相当于goto中的 7、8 行,这里的8、9 行判断循环是否继续,相当于goto中的9、10行。
for循环
for循环的通用形式如下;
for(init-expr ; test-expr ; update-expr)
body-statement
大概在学C的时候会提到for 循环可以用while 来表示:
init-expr
while(test-expr){
body-statement
update-expr
}
程序首先初始化表达式init-expr的值,然后进入循环,再循环中先对测试值test-expr求值,如果为假,则退出循环,否则执行循环体body-statement,并更新表达式update-expr的值。
基于前面讲过的do-while到while的转换,先给出do-whle形式:
然后将它转为goto代码:
和上面一样,我们将用一个实例来说明问题:
考虑用for 循环写的阶乘函数
用for循环写的计算阶乘的函数是从2 开始,这和前面的从1开始不同,不过这并不影响其逻辑实现的结构,这段代码中,for循环的组成如下:
通过上面的分析,可以得到其goto形式:
到现在为止 ,已经在实例中给出了他的C 形式和goto形式,现在请看他的汇编形式:
上面都解释了很多,这里就不在解释这个汇编指令了,通过上面应该都能看懂。
开始的问题
到现在,应该弄清楚文章开头所说的问题了吧
for( i=0 ; i< 10 ; i++)
{
printf(”%i“,i);
}
现在应该了解什么时候执行 i++ 设么时候执行printf(”%i“,i); 了吧。
按照我们的分析,执行顺序应该是这样的:
1、 初始化 i=0
2、判断条件是否满足 i<10 是否成立
3、如果成立则执行循环体printf(”%i“,i); 并执行自加运算 i++
可以这段代码在自己的编译器中运行一下,看首先打印出来的是0 还是1 (按照我们的分析,应该从0 开始答应哦)
小结
其实这篇文章阐述的道理很简单,就是C 语言中循环的实现问题。如果你在linux环境下操作的话,要理解这一部分就更容易了,在linux下可以直观的看到一个程序编译后的汇编形式,在自己电脑上分析自己的程序,这样学起来才更有兴趣,更深刻。
简单介绍:
linux下编译器:GCC、
可执行文件查看器:objdump、还有一个好像是 elfread,不确定了。
如果调试上面的for语句的话,可以用gdb调试器,这样能让程序一步步执行,并且跟踪变量的值。关于linux下的这些命令这里不是本文重点,不再介绍。
全文完。