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