2.1 for循环

考虑这样一个问题:打印1,2,3,…,10,每个占一行。本着“解决问题第一”的思想,很容易写出程序:10条printf语句就可以了。或者也可以写一条,每个数后面加一个“\n”换行符。但如果把10改成100呢?1000呢?甚至这个重复次数是可变的:“输入正整数n,打印1,2,3,…,n,每个占一行。”又怎么办呢?这时可以使用for循环。

程序2-1 输出1,2,3,…,n的值


1  #include<stdio.h>
2  int main()
3  {
4    int n;
5    scanf("%d", &n);
6    for(int i = 1; i <= n; i++)
7      printf("%d\n", i);
8    return 0;
9  }

暂时不用考虑细节,只要知道它是“让i依次等于1,2,3,…,n,每次都执行printf("%d\n",i);”即可。这个“依次”非常重要:程序运行结果一定是1,2,3,…,n,而不是别的顺序。

提示2-1:for循环的格式为:for(初始化;条件;调整)循环体;

在刚才的例子中,初始化语句是“int i=1”。这是一条声明+赋值的语句,含义是声明一个新的变量i,然后赋值为1。循环条件是“i≤n”,当循环条件满足时始终进行循环。调整方法是i++,其含义和i=i+1相同——表示给i增加1。循环体是语句“printf("%d\n",i);”,这就是计算机反复执行的内容。注意循环变量的妙用:尽管每次执行的语句相同,但是由于i的值不断变化,该语句的输出结果也是不断变化的。

提示2-2:尽管for循环反复执行相同的语句,但这些语句每次的执行效果往往不同。

为了更深入地理解for循环,下面给出了程序2-1的执行过程。

当前行:5。scanf请求键盘输入,假设输入4。此时变量n=4,继续。

当前行:6。这是第一次执行到该语句,执行初始化语句int i=1。条件i≤n满足,继续。

当前行:7。由于i=1,在屏幕输出1并换行。循环体结束,跳转回第6行。

当前行:6。先执行调整语句i++,此时i=2,n=4,条件i≤n满足,继续。

当前行:7。由于i=2,在屏幕输出2并换行。循环体结束,跳转回第6行。

当前行:6。先执行调整语句i++,此时i=3,n=4,条件i≤n满足,继续。

当前行:7。由于i=3,在屏幕输出3并换行。循环体结束,跳转回第6行。

当前行:6。先执行调整语句i++,此时i=4,n=4,条件i≤n满足,继续。

当前行:7。由于i=4,在屏幕输出4并换行。循环体结束,跳转回第6行。

当前行:6。先执行调整语句i++,此时i=5,n=4,条件i≤n不满足,跳出循环体。

当前行:8。程序结束。

这个执行过程对于理解for循环非常重要:语句是一条一条执行的。强烈建议教师在课堂上演示单步调试的方法,并打开i和n的watch功能,以帮助学生掌握如何用实验验证上面所介绍的执行过程。观察执行过程时应留意两个方面:“当前行”的跳转(在IDE中往往高亮显示),以及变量的变化。这二者也是编码、测试和调试的重点。根据实际情况,教师可以用IDE(如Code::Blocks)或者文本界面的gdb进行演示。gdb的简明参考见附录A。

提示2-3:编写程序时,要特别留意“当前行”的跳转和变量的改变。

上面的代码里还有一个重要的细节:变量i定义在循环语句中,因此i在循环体内不可见,例如,在第8行之前再插入一条“printf("%d\n",i);”会报错(1)。有经验的程序员总是尽量缩小变量定义的范围,当写了足够多的程序之后,这样做的优点会慢慢表现出来。

提示2-4:建议尽量缩短变量的定义范围。例如,在for循环的初始化部分定义循环变量。

有了for循环,可以解决一些简单的问题。

例题2-1 aabb

输出所有形如aabb的4位完全平方数(即前两位数字相等,后两位数字也相等)。

【分析】

分支和循环结合在一起时功能强大:下面枚举所有可能的aabb,然后判断它们是否为完全平方数。注意,a的范围是1~9,但b可以是0。主程序如下:


for(int a = 1; a <= 9; a++)
  for(int b = 0; b <= 9; b++)
   if(aabb是完全平方数) printf("%d\n", aabb);

请注意,这里用到了循环的嵌套:for循环的循环体自身又是一个循环。如果难以理解嵌套循环,可以用前面介绍的方法——在IDE或gdb中单步执行,观察“当前行”和循环变量a和b的变化过程。

上面的程序并不完整——“aabb是完全平方数”是中文描述,而不是合法的C语言表达式,而aabb在C语言中也是另外一个变量,而不是把两个数字a和两个数字b拼在一起(C语言中的变量名可以由多个字母组成)。但这个“程序”很容易理解,甚至能让读者的思路更加清晰。

这里把这样“不是真正程序”的“代码”称为伪代码(pseudocode)。虽然有一些正规的伪代码定义,但在实际应用中,并不需要太拘泥于伪代码的格式。主要目标是描述算法梗概,避开细节,启发思路。

提示2-5:不拘一格地使用伪代码来思考和描述算法是一种值得推荐的做法。

写出伪代码之后,我们需要考虑如何把它变成真正的代码。上面的伪代码有两个“非法”的地方:完全平方数判定,以及aabb这个变量。后者相对比较容易:用另外一个变量n=a*1100+b*11存储即可。

提示2-6:把伪代码改写成代码时,一般先选择较为容易的任务来完成。

接下来的问题就要困难一些了:如何判断n是否为完全平方数?第1章中用过“开平方”函数,可以先求出其平方根,然后看它是否为整数,即用一个int型变量m存储sqrt(n)四舍五入后的整数,然后判断m2是否等于n。函数floor(x)返回不超过x的最大整数。完整程序如下:

程序2-2 7744问题(1)


#include<stdio.h>
#include<math.h>
int main()
{
  for(int a = 1; a <= 9; a++)
    for(int b = 0; b <= 9; b++)
    {
      int n = a*1100 + b*11; //这里才开始使用n,因此在这里定义n
      int m = floor(sqrt(n) + 0.5);
      if(m*m == n) printf("%d\n", n);
    }
  return 0;
}

读者可能会问:可不可以这样写?if(sqrt(n)==floor(sqrt(n)))printf("%d\n",n),即直接判断sqrt(n)是否为整数。理论上当然没问题,但这样写不保险,因为浮点数的运算(和函数)有可能存在误差。

假设在经过大量计算后,由于误差的影响,整数1变成了0.9999999999,floor的结果会是0而不是1。为了减小误差的影响,一般改成四舍五入,即floor(x+0.5)(2)。如果难以理解,可以想象成在数轴上把一个单位区间往左移动0.5个单位的距离。floor(x)等于1的区间为[1,2),而floor(x+0.5)等于1的区间为[0.5,1.5)。

提示2-7:浮点运算可能存在误差。在进行浮点数比较时,应考虑到浮点误差。

另一个思路是枚举平方根x,从而避免开平方操作。

程序2-3 7744问题(2)


#include<stdio.h>
int main()
{
  for(int x = 1; ; x++)
  {
    int n = x * x;
    if(n < 1000) continue;
    if(n > 9999) break;
    int hi = n / 100;
    int lo = n % 100;
    if(hi/10 == hi%10 && lo/10 == lo%10) printf("%d\n", n);
  }
  return 0;
}

此程序中的新知识是continue和break语句。continue是指跳回for循环的开始,执行调整语句并判断循环条件(即“直接进行下一次循环”),而break是指直接跳出循环(3)

这里的continue语句的作用是排除不足四位数的n,直接检查后面的数。当然,也可以直接从x=32开始枚举,但是continue可以帮助我们偷懒:不必求出循环的起始点。有了break,连循环终点也不必指定——当n超过9999后会自动退出循环。注意,这里是“退出循环”而不是“继续循环”(想一想,为什么),可以把break换成continue加以验证。

另外,注意到这里的for语句是“残缺”的:没有指定循环条件。事实上,3部分都是可以省略的。没错,for(;;)就是一个死循环,如果不采取措施(如break),就永远不会结束。