Chinaunix首页 | 论坛 | 博客
  • 博客访问: 39030
  • 博文数量: 10
  • 博客积分: 246
  • 博客等级: 二等列兵
  • 技术积分: 170
  • 用 户 组: 普通用户
  • 注册时间: 2012-05-15 23:39
文章分类

全部博文(10)

文章存档

2012年(10)

我的朋友

分类: C/C++

2012-05-18 23:26:21

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

现在,我们基本了解if --else 的执行机理了。主要是先判断条件值,从这个例子出发就是第 3 条语句: cmpl  %eax,%edx。学过汇编的同学应该知道,cmpl是一条比较指令,他的执行会影响标志位。做个简单的假设: cmpl     A,B 是比较A,B两个数的大小,比较结果存放在标志位 F 中,当A>B时,F为0,AB 查看后标志位为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下的这些命令这里不是本文重点,不再介绍。

 

全文完。

阅读(1443) | 评论(0) | 转发(1) |
0

上一篇:一分钟上手Makefile

下一篇:浅析代码优化

给主人留下些什么吧!~~