智能程序设计(C语言)


循环结构 (上)

计算机学院    杨已彪

yangyibiao@nju.edu.cn

提纲

  • 为什么需要循环: 三种循环语句

  • while 语句与复合循环体

  • do-while 与输入校验

  • 案例: 平方表、数列求和、整数的位数

  • break、continue 与空语句陷阱

  • 课堂小测: 概念 + 编码


为什么需要循环

有些工作就是"同一件事做很多遍":

  • 打印 1 到 100——手写 100 行 printf?

  • 输入 100 个成绩求平均——声明 100 个变量?

  • 之前讲过的"找最高分"——求用户输入数据的最大值, 当时只能处理固定几个数

计算机最擅长不厌其烦地重复——循环就是把这个能力交给你的语法


三种循环语句

C 有三种循环语句 (控制表达式的玩法下面细讲):

  • while: 先判断, 再执行

  • do-while: 先执行, 再判断

  • for: 最适合计数循环

while 条件 真 循环体 假 先判断,可能 0 次 do-while 循环体 条件 真 假 先执行,至少 1 次 for i = 1 初始化 条件 真 循环体 i++ 步长 假 初始化 → 条件 → 体 → 步长 while / for 先判断;do-while 先执行——三者可互相改写

while语句

while 是最简单的循环语句, 形式为 while (表达式) 语句

  • 表达式是控制表达式(条件表达式); 语句是循环体, 可为单条语句或 {} 复合语句

例: 找 ≥ n 的最小 2 的幂:

#include <stdio.h>
int main(void) {
    int n, i = 1;
    scanf("%d", &n);
    while (i < n)        /* 控制表达式 */
        i = i * 2;       /* 循环体 */
    printf("%d\n", i);
    return 0;
}

n = 10 时, while 的执行过程:

i = 1;        i 现在是 1
i < n?        是, 继续
i = i * 2;    i 现在是 2
i < n?        是, 继续
i = i * 2;    i 现在是 4
i < n?        是, 继续
i = i * 2;    i 现在是 8
i < n?        是, 继续
i = i * 2;    i 现在是 16
i < n?        否, 退出循环
  • 执行过程: 先计算控制表达式; 非零 (true) 就执行循环体并再次测试, 持续到控制表达式变为零才停止


while语句

while (i > 0) {          /* 循环体: 两条语句 */
  printf("%d\n", i);
  i--;
}
while (i < n) {          /* 循环体只有一条语句 */
  i = i * 2;
}
i = 10;
while (i > 0) {
  printf("%d\n", i);
  i--;
}
printf("发射!\n");

循环体必须是一条语句

要执行多条语句, 就用大括号包成一条复合语句:

即使只有一条语句, 建议也用大括号——以后加语句不用补:

看一个火箭发射倒计时的例子: 打印 10、9、…、1, 最后喊出"发射!"

最后一行输出是 发射!


while语句: 注意点与无限循环

  • 循环结束时, 控制表达式一定为假——倒计时循环结束, 说明 i ≤ 0 了

  • 循环体可能一次都不执行——条件在循环体之前先判定

  • 倒计时还有更简洁的写法 (i-- 直接在 printf 里用掉):

while (i > 0) {
  printf("%d\n", i--);
}
  • 控制表达式恒为非零就是无限循环: while (1) { … } 会永远执行, 除非循环体里有 break、goto、return, 或调用终止程序的函数


程序: 打印平方表

现场演示: 打印平方表——用户指定行数, 每行输出"数字 + 它的平方"

⚡ 打开 skeleton.c

样例 (输入 5):

请输入行数: 5
         1         1
         2         4
         3         9
         4        16
         5        25

提示: 两列右对齐——之前的 %10d 字段宽度还记得吗?


程序: 打印平方表 (参考代码)

代码
/* 打印平方表 */
#include <stdio.h>

int main(void)
{
  int i, n;

  printf("请输入行数: ");
  scanf("%d", &n);

  i = 1;
  while (i <= n) {
    printf("%10d%10d\n", i, i * i);
    i++;
  }

  return 0;
}
要点
  • i 从 1 到 n, 循环条件 i <= n——计数循环用 while 的最常见写法

  • 循环体两条语句: 打印一行 + 计数递增——所以用大括号

  • %10d: 占 10 格右对齐, 两列对齐成表


程序: 数列求和

现场演示: 对用户输入的整数数列求和——用户不断输入整数, 直到输入 0 为止, 输出总和

⚡ 打开 skeleton.c

样例:

输入整数 (0 结束): 8 23 71 5 0
总和是: 107

提示: 0 是哨兵值——本身不参与求和, 只用来表示"输完了"


程序: 数列求和 (参考代码)

代码
/* 对数列求和 */
#include <stdio.h>

int main(void)
{
  int n, sum = 0;

  printf("输入整数 (0 结束): ");
  scanf("%d", &n);
  while (n != 0) {
    sum += n;
    scanf("%d", &n);
  }
  printf("总和是: %d\n", sum);

  return 0;
}
要点
  • 先读一个数, 不是 0 就累加、再读下一个——读一个, 判一个, 加一个

  • sum 初值必须为 0 (累加的起点)

  • 哨兵值模式: 用特殊值标记"输入结束", 循环条件就是 n != 0


任务 1: 猜数字 (循环版)

任务: 程序心里想好一个 1~100 的整数 (骨架里已设为 42), 你反复输入猜测, 程序提示"太大"或"太小", 直到猜中; 最后输出猜了几次

样例:

猜一个 1~100 的整数: 50
太大了
猜一个 1~100 的整数: 25
太小了
猜一个 1~100 的整数: 42
猜对了! 你猜了 3 次

提示:

  • 先读一次, 再进入 while——循环条件就是"没猜中"

  • 每轮: 比较并提示 → 计数 → 再读下一个; 猜中那次不在循环体里, 计数别漏

⚡ 在 VS Code 中打开 task1-guess.c (骨架)

任务 1: 参考代码与要点

/* guess.c: 猜数字循环版 */
#include <stdio.h>
int main(void) {
  int secret = 42, guess, count = 0;
  printf("猜一个 1~100 的整数: ");
  scanf("%d", &guess);
  while (guess != secret) {
    if (guess > secret) printf("太大了\n");
    else                printf("太小了\n");
    count++;
    printf("猜一个 1~100 的整数: ");
    scanf("%d", &guess);
  }
  count++;
  printf("猜对了! 你猜了 %d 次\n", count);
  return 0;
}
  • 循环条件 guess != secret: 猜中就退出——沿用"先读一次, 再判断"的哨兵模式

  • 循环体末尾再读一次, 下一轮判断的才是新输入; 忘了这句就是死循环

  • 计数: 循环体里的 count++ 只数"猜错"的次数, 猜中那次在循环外, 所以结束后再 count++

  • 也可以用 while (1) + 猜中时 break——下一节的 break 会让这种写法更直接


do语句

一般形式: do 语句 while (表达式);

最典型的用途: 输入校验——必须先读一次, 才知道合不合法:

#include <stdio.h>
int main(void) {
    int n;
    do {
        printf("请输入一个正整数: ");
        scanf("%d", &n);
    } while (n <= 0);
    printf("你输入了 %d\n", n);
    return 0;
}
  • 执行顺序: 先执行循环体 → 再算控制表达式 → 非零再来一轮

  • 与 while 的唯一区别: 循环体至少执行一次——0 也是一位数, 至少拆一次位

  • "先读后判"的场景 while 写起来别扭 (得先预读一次), do-while 刚刚好——对比数列求和的预读写法

  • 建议给所有 do 语句都加大括号——没有大括号的 do 容易被误读成 while 的开头:

do
    printf("%d\n", i--);
while (i > 0);   /* 这个 while 不是新循环 */

任务 2: 各位数字之和

任务: 输入一个非负整数, 输出它各位数字之和; 0 也要能正确处理

样例:

输入一个非负整数: 12345
各位数字之和 = 15
输入一个非负整数: 0
各位数字之和 = 0

提示:

  • 拆位老办法: % 10 取末位, / 10 去掉末位

  • 至少要做一次——do-while 正合适; 想想输入 0 时 while 版会怎样

⚡ 在 VS Code 中打开 task2-digit-sum.c (骨架)

任务 2: 参考代码与要点

/* digitsum.c: 各位数字之和 */
#include <stdio.h>
int main(void) {
  int n, sum = 0;

  printf("输入一个非负整数: ");
  scanf("%d", &n);

  do {
    sum += n % 10;
    n /= 10;
  } while (n > 0);

  printf("各位数字之和 = %d\n", sum);
  return 0;
}
  • n % 10 取末位、n /= 10 缩位, 循环到 n 变 0——和"计算整数的位数"是同一套拆位

  • 用 do-while: 0 也是一位数, 循环体至少执行一次; 换成 while (n > 0), 输入 0 时一次都不进 (本题 sum 初值 0 碰巧对, 换成统计位数就错了)

  • 循环结束后 n 已经被改成 0; 还要用原数的话, 先复制一份 (下一节"位数"的例子就是这么做的)

  • sum 是累加器, 初值必须为 0——和数列求和的 sum 一样


程序: 计算整数的位数

现场演示: 输入一个非负整数, 输出它是几位数

⚡ 打开 skeleton.c

样例:

输入一个非负整数: 60
它有 2 位数字

提示: 每除以 10 一次, 位数就多 1——直到除成 0。想想为什么这里用 do-while 比 while 更合适?


程序: 计算整数的位数 (参考代码)

代码
/* 计算整数的位数 */
#include <stdio.h>

int main(void)
{
  int digits = 0, n;

  printf("输入一个非负整数: ");
  scanf("%d", &n);

  do {
    n /= 10;
    digits++;
  } while (n > 0);

  printf("它有 %d 位数字\n", digits);
  return 0;
}
要点
  • 思路: 反复除以 10 直到变 0, 除的次数就是位数 (60 → 6 → 0, 两次 → 2 位)

  • 为什么用 do-while: 每个整数至少有一位数字——输入 0 时 while 版会输出 0 位 (错的!), do-while 先除一次正好

  • digits 初值 0, 每轮 +1——计数循环的惯用写法


break语句

break 把控制从 switch 中转出, 也可跳出循环。

检查 n 是否为素数的循环, 找到因子后立即 break:

#include <stdio.h>
int main(void) {
    int n = 13, d = 2;
    while (d < n) {
        if (n % d == 0) {
            break;
        }
        d++;
    }
    if (d < n) {
        printf("%d 能被 %d 整除\n", n, d);
    } else {
        printf("%d 是素数\n", n);
    }
    return 0;
}

循环终止后, 用 if 判断: 提前终止 → n 不是素数; 正常终止 → n 是素数。

break 适合退出点位于循环体中间的场景 (读取输入并在特定值时终止):

#include <stdio.h>
int main(void) {
    int n;
    while (1) {
        printf("输入一个数 (0 结束): ");
        scanf("%d", &n);
        if (n == 0) {
            break;
        }
        printf("%d 的立方是 %d\n", n, n * n * n);
    }
    return 0;
}
  • break 只跳出最内层的一层 while/do/for/switch

  • switch 里的 break 只结束 switch 本身; 要跳出外层 while, 需在外层另写 break


continue语句

任务: 读入 10 个整数, 计算 100/i 的和——i 为 0 时除零会崩溃, 必须跳过

用 continue 的版本:

n = 0;
sum = 0;
while (n < 10) {
  scanf("%d", &i);
  n++;             /* 先计数: continue 不能跳过它 */
  if (i == 0) {
    continue;      /* 跳过除零 */
  }
  sum += 100 / i;
}

不用 continue 的版本:

n = 0;
sum = 0;
while (n < 10) {
  scanf("%d", &i);
  n++;
  if (i != 0) {
    sum += 100 / i;
  }
}
  • break 跳出循环; continue 跳到本轮循环体末尾, 直接进入下一轮 (循环照常继续)

  • 效果: 读到 0 必须跳过——否则 100 / i 除零崩溃; continue 的必要性不依赖题目规定, 而是程序的生死问题

  • 另一个区别: break 可用于 switch 和循环; continue 只能用于循环


空语句: 一个分号的代价

空语句: 只有一个分号 ;, 什么都不做

  • 意外写出的空语句是经典 bug——多打的一个分号会让循环体/if 体变成"空":

while (i < n);        /* BUG: 循环体是空语句 → 死循环! */
  sum += i++;         /* 这行其实只执行一次 */

if (d == 0);          /* BUG: if 的体是空语句 → 条件成立时什么都不做 */
  printf("zero");
  • 编译器通常会警告 (empty body in a while), 但不要依赖警告

  • 好习惯: 循环体和 if 后面永远加大括号, 哪怕只有一条语句


课堂小测

  1. (概念) while 和 do-while 唯一的区别是什么?
  2. (概念) break 和 continue 分别把控制转移到哪里?
  3. (编码) 输入整数直到 0 为止, 输出其中最大值



未完待续