
7.4 while()...循环语句
while()...语句的语法是:

程序首先求continuation_expression的值,如果是true,就是执行statement_body(语句体),然后再度求continuation_expression的值,并重复此过程。如果continuation_expression是false,那么循环就结束了,程序接着执行整个循环结构之后的那条语句。如果程序首次判断continuation_expression的时候就发现它的值是false,那么根本不会执行statement_body。
statement_body这一部分可以是一条简单的语句,甚至可以是一条null语句(空语句,也就是不带任何表达式,直接以;结尾的语句)。然而,在大多数情况下,它都是一条复合语句。请注意,while()...本身是不加分号的。如果statement_body是一条简单语句,那么这条简单语句要以分号结尾,但如果statement_body是一条复合语句,那么该语句会用一对花括号括起来,这对花括号的末尾不加分号。
另外要注意,在statement_body这一部分里面,你必须设法修改continuation_expression所用到的值。假如不这么做,那么循环一旦启动,就会一直执行。这种情况叫作无限循环(infinite loop)。如果continuation_expression是通过计数器来判断循环是否应该继续的,那你就必须在循环体(也就是statement_body部分)里面设法修改计数器的值,让循环能够适时地终止。
现在回到高斯求和问题。这次我们用while()...循环来编写函数,让这个函数接受名为N的参数,以表示有待求和的这个整数序列里面值最大的数。该函数返回1与N之间的各整数之和。我们需要用一个变量(即sum变量)来保存求和结果,这个变量的初始值是0。另外,我们还需要用一个计数器(即num变量)来控制迭代次数,这个计数器的初始值也是0。它会依次取从0到(N-1)之间的各个整数。循环条件是:计数器是否小于N?如果计数器本身的值已经变得与N相等了,那么这个循环条件就不再成立(因为N本身并不小于N),于是循环就会结束。在循环体中,我们把计数器所对应的整数值(也就是num+1)累计到表示求和结果的变量里面,然后递增该计数器。循环完成之后,我们把求和结果返回给调用方。
这个sumNviaWhile()函数写在gauss_loops.c程序里面,它的代码是这样的:

这段代码中有一个小问题需要注意,也就是容易出现off-by-one(多算一个或少算一个)的现象。这个问题不仅容易发生在编写C程序的时候,你在用其他语言做开发时也容易碰到,只不过形式有所区别。num计数器会从0取到N-1,从而让循环迭代N次。其实还有另外两种写法,一种是让num从1取到N,但那样写的话,循环条件就不是num<N了,而应该改成num<N+1(或者num<=N,同时还要把循环体里面的num+1改成num)。第二种是让num从0取到N,并把循环条件改成num<=N(或者num<N+1,同时还要把return sum;改成return sum-num;)。第二种改法虽然也能算出正确答案,但是循环体的迭代次数是N+1,而不像现在这种写法以及刚才提到的第一种改法那样,只需迭代N次即可。比方说,如果N是10,那么第二种改法会让计数器从0取到10,使得循环体总共需要迭代11次。
我们让计数器从0开始计算是有理由的,因为这跟C语言处理数组下标(array index,也叫数组索引)的方式相符,这个问题会在第11章讲解。大家现在可能觉得让计数器或下标从0开始计算似乎有点奇怪,但这其实是C语言一贯的原则。你应该熟悉这种方式,这样可以在计算数组下标与做指针加法的时候避开许多错误。
为了减少与计数器的取值范围有关的错误,笔者发现,我们最好把表达式能够成立的这个取值范围用注释标出来,用以表示如果计数器或下标的值位于该范围内,那么条件表达式就是成立的。这样写可以提醒自己在计数器上执行必要的操作,以便将它的值转化成我们执行运算时需要的那个值,比方说刚才那个函数就会给计数器的值加1,以确定有待计入求和结果的这个整数。
除了这种写法,还有一种写法也能解决高斯求和问题(C语言跟大多数编程语言一样,都允许你用不同的写法来解决同一个问题)。这种写法还是用while()...循环来实现,但不让计数器递增,而是让它递减,这样就不用专门用一个变量来表示计数器了,而是可以直接用函数的参数N充当计数器。前面说过,程序会把调用方在调用函数时所传入的参数值复制到函数的相应参数里面,因此,对于这个函数来说,它的参数N实际上也可以像局部变量一样使用,我们可以用这个参数充当计数器。这次让它逐渐变小,而不是像刚才那样让它逐渐变大。计数器的值会从N逐渐降为1。当计数器是0的时候,我们让这个循环终止,为了表示这个逻辑,我们不需要把while循环的条件表达式写成N!=0或N>0,而是可以直接写成N,因为N一旦为0,这个条件表达式的结果就是false,这会促使程序结束while循环。
我们把决定循环是否应该继续的这个continuation_expression表达式直接写成了N,这样只要N不是0,循环就会继续执行。其实本来也可以写成while(N>0)(或while(N!=0)),那样写虽然没什么必要,但会让代码更加明确。另外,这个版本的函数还有一个小优势,就是不会出现off-by-one(多算一个或少算一个)的问题,而且,这次的计数器本身正是我们在做求和运算时需要用到的这个值,因此不需要像原来那样先调整计数器的值,然后再用它执行运算。
这次的sumNviaWhile2()函数写在gauss_loops2.c程序中,它的代码是这样的:

在这两种写法里面,有没有哪种写法肯定会比另一种好呢?其实并没有。而且对于我们这个例子来说更不会出现这样的情况,因为这个例子实在是太简单了。如果循环体(也就是statement_body这一部分)变得比较复杂,那么其中一种写法读起来可能会比另一种更加清晰。笔者在这里用两种写法来解决同一个问题是想让大家从不同的角度思考这个问题,当然,这样写出来的代码在清晰程度上可能也会稍有区别。对于本例来说,这两种写法之间的差异在于如何点算有待求和的这些整数是从小到大累加,还是从大到小累加。