循环语句

第六章 循环语句

1.while语句

用递归求n!的方法,其实每次递归都是在重复做同样一件事,就是把n乘到(n-1)!上然后把结果返回。虽说是重复,但每次做都稍微有一点区别(n的值不一样),这种每次都有点区别的重复工作称为(Iteration)。虽然迭代用递归来做就够了,但C语言提供了循环语句使迭代程序写起来更方便。例如factorial用while语句可以写成:

 

像if语句一样,while由一个控制表达式和一个子语句组成,子语句可以是由若干条语句组成的语句块。如果控制表达式的值为真,子语句就被执行,然后再次测试控制表达式的值,如果还是真,就把子语句再执行一遍,再测试控制表达式的值 . . . . . . 这种控制流程称为循环(Loop),子语句称为循环体。如果某一次测试控制表达式的值为假,就跳出循环执行后面的return语句,如果第一次测试控制表达式的值就是假,那么直接调到return语句,循环体一次都不执行。

变量result在这个循环中的作用就是累加器(Accumulator),把每次循环的中间结果累积起来,循环结束后得到的累积值就是最终结果,由于这个例子是用乘法来累积的,所以result初值为1,如果是用加法来累积那么result初值应该是0。变量n是循环变量(Loop Variable),每次循环要改变它的值,在控制表达式中要测试它的值,这两点合起来起到控制循环的次数的作用。

         在整个递归调用过程中,虽然分配和释放了很多变量,但是所有的变量都只在初始化时赋值,没有任何变量的值发生过改变,而上面的循环程序则是通过对n和result这两个变量多次赋值来达到同样目的的。前一种思路称为函数式编程(Functional Programming),而后一种思路称为命令式编程(Imperative Programming)。

         正如递归函数如果写得不小心就会变成无穷递归一样,循环如果写得不小心就会变成无限循环(Infinite Loop)或者叫死循环。如果while语句的控制表达式永远为真就是一个死循环,例如while(1){ . . .}。在写循环时要小心检查你写的控制表达式有没有可能取值为假,除非你故意写死循环(有些时候这是必要的)。

2.do/while语句

do/while语句的格式是:

 

它和while类似,其中的语句可以是一个语句块,构成循环体。只不过while是先测试控制表达式的值再执行循环体,而do/while是先执行循环体再测试控制表达式的值。如果控制表达式的值一开始就是假,while的循环体一次都不执行,而do/while的循环体至少会执行一次。其实只要有while这一种循环就足够了,do/while循环和后面要讲的for循环体至少会执行一次。其实只要有while这一种循环就足够了,do/while循环和后面要讲的for循环都可以改写成while循环,只不过有些情况下用do/while或for循环写起来更简便,代码更易读。上面的factorial也可以改用do/while来写:

 

注意do/while这种形式在while(控制表达式)后面一定要加;号,否则编译器无法判断这是一个do/while循环的结尾还是另一个while循环的开头。写循环时一定要注意循环即将结束时控制表达式的临界条件是否准确,上面的循环条件如果写成i<n就错了,当i==n时跳出循环,最后的结果中就少乘了一个n。虽然变量名应该尽可能起的有意义一些,不过i、j、k给循环变量起名是很常见的。

3.for语句

以上我们在while或do/while循环中使用循环变量,其实使用循环变量最常用的是for循环这种形式。For语句的格式为:

 

如果不考虑语句中包含continue语句的情况,这个for循环等价于下列的whie循环:

 

从这种等价形式来看,控制表达式1和3都可以为空,但控制表达式2是必不可少的,例如,for(;1;){ . . . }等价于while(1){ . . .}死循环。C语言规定,如果控制表达式2为空,则当作控制表达式2的值为真,因此,死循环也可以写成for( ; ; ){ . . . }。

上节的factorial也可以改用for循环来写:

 

其中++i这个表达式相当于i = I + 1,++称为前缀自增运算符(Prefix Increment Operator),类似地,--称为前缀自减运算符(Prefix Decrement Operator),--i相当于i = i – 1。如果把++i这个表达式看作一个函数调用,除了传入一个参数返回一个值(等于参数加1)之外,还产生一个Side Effect,就是把变量i的值增加了1。

使用++、--运算符会使程序更加简洁,但也会影响程序的易读性。

         问题:a+++++b这个表达式如何理解?

         编译的过程分为词法解析和语法解析两个阶段,在词法解析阶段,编译器总是从前到后找最长的合法Token。把这个表达式从前到后分析,变量名a是一个Token,运算符++是一个Token,再往后找又有两个+号,根据最长匹配原则,编译器绝不会止步于一个+号,而一定会把这两个+号当作一个Token。然后进入下一阶段语法解析,a是一个表达式,表达式++还是表达式,表达式再++还是表达式,表达式+b还是表达式,语法上没有问题。最后编译器会做一些基本的语义分析,这时就有问题了,++运算要求操作数能做左值,a能做左值,所以a++没问题,但表达式a++的值只能做右值,不能再++了,所以最终编译器会报错。

         C99引入一种新的for循环,规定控制表达式1的位置可以有变量定义。例如上例的循环变量i可以只在for循环中定义:

 

如果这样定义,那么变量i只是for循环中的局部变量而不是整个函数的局部变量,相当于语句块中的局部变量,因此在循环结束后不能再使用i这个变量了。这个程序用gcc编译要加上选项-std=c99。

4.break和continue语句

Break语句用来跳出switch语句块,也可以跳出循环体; continue语句也用来中止当前循环,和break语句不同的是,continue语句中止当前循环后又回到循环体的开头准备再次执行循环体。对于while和do/while,continue之后测试控制表达式,如果值为真则继续执行下一次循环;对于for循环,continue之后首先计算控制表达式3,然后测试控制表达式2,如果值为真则继续执行下一次循环。例如下面的代码打印1到100之间的素数:

 

is_prime函数从2到n-1依次检查有没有能被n整除的数,如果有就说明n不是素数,立刻跳出循环而不执行++i。因此,如果n不是素数,则循环结束结束后i一定小于n,如果n是素数,则循环结束后i一定等于n。注意检查临界条件:2应该是素数,如果n是2,则循环体一次也不执行,但是i的初始值就是2,也等于n,在程序中也判定为素数。

         在主程序中,从1到100依次检查每个数是不是素数,如果不是素数,并不直接跳出循环,而是++i后继续执行下一次循环,因此用continue语句。注意主程序的局部变量i和is_prime中的局部变量i是不同的两个变量,其实在调用is_prime函数时,主程序中的局部变量i的值和参数n的值相等。

5.嵌套循环

前面求素数的例子在循环中调用一个函数,而那个函数又是一个循环,这其实是一种嵌套循环。用嵌套循环求1-100的素数:

 

现在内循环的循环变量就不能再用i了,而是改用j,原来程序中is_prime函数的参数n现在直接用i代替。在有嵌套循环的情况下,break只能跳出最内层的循环或switch语句,continue也只能终止最内层循环并回到该循环的开头。

6.goto语句

要实现无条件跳转就用goto语句。Break只能跳出最内层的循环,如果在一个嵌套循环中遇到某个错误条件需要立即跳到循环之外的某个地方做出错处理,就可以用goto语句。

例如:

 

这里的error:叫做标号(Label),给标号起名字也遵循标识符的命名规则。我们在“switch”语句中学过的case和default后面也是跟一个冒号,在语法结构中也起标号的作用。

         goto语句过于强大,从程序中的任何地方都可以无条件跳转到任何其他地方,只要给哪个地方起个标号就行,唯一的限制是goto只能跳转到同一个函数的某个符号处,而不能别的函数里。所以,滥用goto语句会使程序的控制流程非常复杂,可读性很差。著名的计算机科学家Edsger W.Dijkstra最早指出编程语言中goto语句的危害,提倡取消goto语句。goto语句不是必须存在的,显然可以用别的办法替代,比如上面的代码段可以改写为:

 

通常goto语句只用于在函数末尾做出处理(例如释放先前分配的资源、恢复先前改动过的全局变量等),函数中任何地方出现了错误条件都可以立即跳到函数末尾,处理完之后函数返回。比较上面两种写法,用goto语句还是方便很多。但是除了这个用途之外,在任何场合都不要轻易考虑使用goto语句。

posted @ 2017-12-29 14:14  学习与生活  阅读(354)  评论(0编辑  收藏  举报