
7.2 重复执行代码
我们经常需要重复执行一系列语句。比方说,我们可能要对某一组值里面的每个值都执行某项运算,或者要用这一组里面的所有值来完成一项计算。对于这样一组值来说,我们可能还要迭代它们,以便在其中搜寻想找的那个值,或者要计算这一组里面一共有多少个值,另外,我们有可能想调整这一组值,例如给它们排列顺序。
许多方法都能够实现这样的需求。其中最简单、最僵化的一种叫作蛮力法(brute-force method,也叫暴力法)。无论你用的是哪种编程语言,都可以通过这种方法来重复执行某段代码。另外一种办法比蛮力法更为灵活,也就是对集合进行迭代或反复循环。C语言提供了三种相关的循环语句:while()...、for()...与do...while()。每种语句都有一条控制表达式[control expression,或者叫作延续表达式(continuation expression)]以及一个循环体(loop body)。这三种语句里面最通用的一种是while()...循环。另外还有一种古老的循环方式是通过goto语句跳转到相关的标签。与其他一些语言不同,C语言没有repeat...until()语句[1],然而这种语句的功能很容易就可以通过其他形式的语句实现。
每种循环语句都有两个基本部件:
□循环延续表达式
□循环体
循环延续表达式如果是true,那么就执行循环体,然后再回到循环延续表达式,继续求该表达式的值。如果还是true,那么就再次执行循环体,然后继续判断循环延续表达式,直到它的值是false为止。此时循环结束,程序从循环之后的那条语句开始往下执行。
循环语句的延续表达式通常分成两类:
□根据计数器来判断循环是否应该继续。这种循环的迭代次数取决于某个计数器的值。我们提前就知道这样的循环应该迭代多少次。我们可以在循环过程中递增或递减这个计数器,让它在经过一定次数的迭代之后,使延续表达式取到false值,从而让程序跳出循环。
□根据某个条件或某个标记值来决定循环是否应该继续。这种循环的迭代次数取决于某个条件是否依然成立,或者程序是否还没有碰到某个标记值。我们提前并不清楚这样的循环需要迭代多少次。标记值(sentinel)是一种用来判断循环是否完成的值,程序必须先遇到这样的值,才算执行完这个循环。
本章我们要讲的是由计数器所控制的循环。等后面说到如何从控制台获取输入信息,以及如何读取文件中的输入数据时,我们再谈由标记值所控制的循环。
C语言还提供了其他几种控制循环的办法,例如break与continue。其中的break,我们在第6章讲解switch()...语句时提到过。如果普通的计数器或标记值没有办法满足某些特定的循环需求,那我们就会考虑通过这些办法调整循环的执行逻辑。
笔者想通过一个问题来把迭代与重复讲解得更有趣一些,这个问题就是数学家高斯(Gauss,1777—1855)小时候巧妙解决过的连加问题。高斯读小学的时候,有一次,老师为了填补下课前的空闲时间,让大家把1~100(或者从1到某个整数)的所有整数加起来。其他人都在一个一个地往上加(也就是所谓蛮力法),但高斯却发现了一条规律,从而立刻求出了最终结果。这条规律是:

公式中的n是指从1开始的这一系列自然数里面最大的那个数。
在本章中,我们就以高斯小时候面对的连加题为例来讲解循环。我们首先要说的是如何用C语言来表达高斯发现的这个巧妙规律。然后,我们开始研究各种以编程方式(而非数学方式)所构造的解法。第一是蛮力法,这当然是高斯当时不愿意采用的那种方法。第二是循环法,我们要演示如何用C语言中的各种循环结构来解决这道题。最后我们要讲怎样用goto语句来实现这个需求,这种方案不太好写。
下面这段代码是一个名为sumNviaGauss()的C函数,它运用高斯发现的连加公式来解决求和问题:

调用函数的时候需要输入参数N。函数求出的结果是从1~N的各整数之和。这个函数是gauss_bruteforce.c程序的一部分,该文件包含一些链接,那些链接所指向的网页很好地解释了这条连加求和规律的原理,以及这个问题的各种变化形式。笔者就不在这里展开讲解这些内容了。大家可以下载gauss_bruteforce.c文件并访问其中的网址。
注意,N*(N+1)/2这个式子中的N+1必须用一对括号括起来,因为它左边的乘法与右边的除法其优先级都比这一部分里面的加法要高,假如不括起来,那么程序就会把N+1中的N与1分别跟左边的N*与右边的/2相结合,变成计算N*N与1/2的和。只有括起来,才能计算出我们想要的结果。
为什么要在开始讲解循环之前先给出这种通过公式来求解的方案呢?C语言提供了许多强大的语句,让我们能够执行各种复杂的运算以解决复杂的数学问题。然而大家必须注意,有的时候,其实本来就存在某个比较通用的公式或算法(algorithm),能够更简单地解决这个问题,而不需要我们再去重新创造。因此,每个程序员都应该看看Numerical Recipes in X一类的书(其中的X指的是C、Fortran或C++),这些书会告诉你如何用X语言实现已有的公式或算法,从而解决各种复杂的数学问题,这些数学问题都是相当有意义而且解决起来比较费劲的问题,有许多数学家、科学家、工程学家、计算机学家以及运筹学家都在研究它们的解法,如果你不去参考他们的成果,而是自己探索,那可能会吃苦头。
顺便说一下,笔者应该指出的是,自己在研究计算机科学时遇到过某些特别有意义的算法,而那些算法是由研究运筹学的人所提出的。他们似乎总是想解决某些极其困难然而又极其重要的问题。当然,这个话题已经超出了本书的讨论范围。
虽然高斯当年不想用蛮力法来解决问题,但有时我们只能采用这种办法来实现,它在某些情况下甚至是最好的解法,只不过这种情况不多。下一节我们讲解蛮力法。
[1] 意思是反复执行...所表示的这一部分,直到until()条件成立为止。——译者注