和if
语句类似,while
语句由一个控制表达式和一个子语句组成,子语句可以是由若干条语句组成的语句块。
语句 → while (控制表达式) 语句
如果控制表达式的值为真,子语句就被执行,然后再次测试控制表达式的值,如果还是真,就把子语句再执行一遍,再测试控制表达式的值……这种控制流程称为循环(Loop),子语句称为循环体。如果某次测试控制表达式的值为假,就跳出循环执行后面的return
语句,如果第一次测试控制表达式的值就是假,那么直接跳到return
语句,循环体一次都不执行。
可见,递归能解决的问题用循环也能解决,但解决问题的思路不一样。用递归解决这个问题靠的是递推关系n!=n·(n-1)!,用循环解决这个问题则更像是把这个公式展开了:n!=n·(n-1)·(n-2)·…·3·2·1。把公式展开了理解会更直观一些,所以有些时候循环程序比递归程序更容易理解。但也有一些公式要展开是非常复杂的甚至是不可能的,反倒是递推关系更直观一些,这种情况下递归程序比循环程序更容易理解。此外还有一点不同:看,在整个递归调用过程中,虽然分配和释放了很多变量,但所有变量都只在初始化时赋值,没有任何变量的值发生过改变,而上面的循环程序则通过对n
和result
这两个变量多次赋值来达到同样的目的。前一种思路称为函数式编程(Functional Programming),而后一种思路称为命令式编程(Imperative Programming),这个区别类似于第 1 节 “程序和编程语言”讲的Declarative和Imperative的区别。函数式编程的“函数”类似于数学函数的概念,回顾一下所讲的,数学函数是没有Side Effect的,而C语言的函数可以有Side Effect,比如在一个函数中修改某个全局变量的值就是一种Side Effect。第 4 节 “全局变量、局部变量和作用域”指出,全局变量被多次赋值会给调试带来麻烦,如果一个函数体很长,控制流程很复杂,那么局部变量被多次赋值也会有同样的问题。因此,不要以为“变量可以多次赋值”是天经地义的,有很多编程语言可以完全采用函数式编程的模式,避免Side Effect,例如LISP、Haskell、Erlang等。用C语言编程主要还是采用Imperative的模式,但要记住,给变量多次赋值时要格外小心,在代码中多次读写同一变量应该以一种一致的方式进行。所谓“一致的方式”是说应该有一套统一的规则,规定在一段代码中哪里会对某个变量赋值、哪里会读取它的值,比如在会讲到访问errno
的规则。
递归函数如果写得不小心就会变成无穷递归,同样道理,循环如果写得不小心就会变成无限循环(Infinite Loop)或者叫死循环。如果while
语句的控制表达式永远为真就成了一个死循环,例如while (1) {...}
。在写循环时要小心检查你写的控制表达式有没有可能取值为假,除非你故意写死循环(有的时候这是必要的)。在上面的例子中,不管n
一开始是几,每次循环都会把n
减掉1,n
越来越小最后必然等于0,所以控制表达式最后必然取值为假,但如果把这句漏掉就成了死循环。有的时候是不是死循环并不是那么一目了然:
- while (n != 1) {
- if (n % 2 == 0) {
- } else {
- n = n * 3 + 1;
- }
如果n
为正整数,这个循环能跳出来吗?循环体所做的事情是:如果n
是偶数,就把n
除以2,如果n
是奇数,就把n
乘3加1。一般来说循环变量要么递增要么递减,可是这个例子中的n
一会儿变大一会儿变小,最终会不会变成1呢?可以找个数试试,例如一开始n
等于7,每次循环后n
的值依次是:7、22、11、34、17、52、26、13、40、20、10、5、16、8、4、2、1。最后n
确实等于1了。读者可以再试几个数都是如此,但无论试多少个数也不能代替证明,这个循环有没有可能对某些正整数是死循环呢?其实这个例子只是给读者提提兴趣,同时提醒读者写循环时要有意识地检查控制表达式。至于这个循环有没有可能是死循环,这是著名的3x+1问题,目前世界上还无人能证明。许多世界难题都是这样的:描述无比简单,连小学生都能看懂,但证明却无比困难。
2、编写程序数一下1到100的所有整数中出现多少次数字9。在写程序之前先把这些问题考虑清楚:
这个问题中的循环变量是什么?
这个问题中的累加器是什么?用加法还是用乘法累积?