智能程序设计(C语言)


循环结构 (下)

计算机学院    杨已彪

yangyibiao@nju.edu.cn

提纲

  • for 语句: 形式、常见用法与陷阱

  • 省略表达式与 C99 的 for

  • 逗号运算符

  • 嵌套循环: 打印星号塔

  • 循环惯用法与案例: 累加累乘、素数、非完全平方数

  • 课堂小测: 概念、编码、纠错与分析


for语句

for 特别适合次数已知的循环 ("重复 n 次"型), 一般形式:

for (表达式1; 表达式2; 表达式3) 语句
#include <stdio.h>
int main(void) {
    int i;
    for (i = 10; i > 0; i--) {
        printf("%d\n", i);
    }
    printf("发射!\n");
    return 0;
}

三个表达式各司其职:

  • 表达式1: 初始化——只在循环开始前执行一次

  • 表达式2: 条件——非零就继续, 为零结束

  • 表达式3: 递增/递减——每轮最后执行

三件事排在一行, 起点、终点、步长一眼看清——这就是 for 的主场


for语句

表达式1 和表达式3 都是语句, 它们的值无关紧要——起作用的是副作用 (赋值、自增自减)

所以表达式3 不一定非要是 i++/i--——步长 2 也行 (如右):

表达式3 写 i-- 还是 --i 结果一样 (只看副作用, 不看值); 但在复杂表达式里二者不同——第 3 周讲过

/* 求 2+4+6+...+n 的偶数和 */
#include <stdio.h>
int main(void) {
    int n = 100, sum = 0;
    int i;
    for (i = 2; i <= n; i += 2) {
        sum += i;
    }
    printf("偶数和 = %d\n", sum);
    return 0;
}

for语句常见用法

for 最适合向上计数或向下计数, 四种常见写法:

/* 从0到n-1, 共n次 */  for (i = 0; i < n; i++)
                           printf("%d ", i);
/* 从1到n, 共n次 */    for (i = 1; i <= n; i++)
                           printf("%d ", i);
/* 从n-1倒数到0 */     for (i = n - 1; i >= 0; i--)
                           printf("%d ", i);
/* 从n倒数到1 */       for (i = n; i > 0; i--)
                           printf("%d ", i);

输出依次为 0 1 2 …、1 2 3 …、n-1 … 0、n … 1

常见的 for 错误:

  • 方向写反: 向上计数用 </<=, 向下计数用 >/>=——把 > 写成 < 方向就反了

  • 把比较符写成 ==: 初始值就不成立, 循环体一次都不执行

  • 差一错误 (off-by-one): 把 i < n 写成 i <= n 会多循环一次


演示: 浮点计数循环的陷阱

#include <stdio.h>
int main(void) {
    int i;
    double x;
    for (i = 0, x = 0.0; i <= 10; i++, x += 0.1) {
        printf("%2d 次累加后: %.17f\n", i, x);
    }
    return 0;
}

运行结果 (节选):

 1 次累加后: 0.10000000000000001
 2 次累加后: 0.20000000000000001
 3 次累加后: 0.30000000000000004
...
10 次累加后: 0.99999999999999989
  • 0.1 在二进制里是无限循环小数 (第2周讲过), 累加 10 次并不是 1.0

  • 若用 for (x = 0.0; x != 1.0; x += 0.1): 第 10 次后 x 并不等于 1.0, 循环停不下来 (或次数不对)

  • 教训: 不要用浮点数计数/判等; 循环次数用整数控制、浮点只参与运算; 比较浮点用 x < 1.0 而不是 x != 1.0


for语句省略表达式 (省略1)

省略表达式 1: 初始化挪到循环前

#include <stdio.h>
int main(void) {
    int n = 100, i = 1, sum = 0;
    for (; i <= n; ++i) {
        sum += i;
    }
    printf("1+...+%d = %d\n", n, sum);
    return 0;
}
  • 循环变量的初值已经在前面算好了, 表达式 1 的位置就空着 (分号照写)

  • 运行结果: 1+...+100 = 5050

  • 常见于初值来自前面计算结果的循环


for语句省略表达式 (省略3)

省略表达式 3: 递增/递减写进循环体

#include <stdio.h>
int main(void) {
    int n = 100, i, sum = 0;
    for (i = 1; i <= n;) {
        sum += i++;
    }
    printf("1+...+%d = %d\n", n, sum);
    return 0;
}
  • 递增挪进循环体 (藏在 sum += i++ 里), 表达式 3 的位置空着

  • 风险: 循环体必须保证条件最终变假——忘了递增就是死循环

  • 运行结果: 同样 1+...+100 = 5050


for语句省略表达式 (省1+3)

省 1 又省 3 的 for: 两个分号之间只剩条件:

#include <stdio.h>
int main(void) {
    int n = 100, i = 1, sum = 0;
    for (; i <= n;) {
        sum += i++;
    }
    printf("1+...+%d = %d\n", n, sum);
    return 0;
}

等价 while 版:

#include <stdio.h>
int main(void) {
    int n = 100, i = 1, sum = 0;
    while (i <= n) {
        sum += i++;
    }
    printf("1+...+%d = %d\n", n, sum);
    return 0;
}
  • 两版输出相同: 1+...+100 = 5050——都省了初始化与递增, 语义完全等价

  • 这种场合 while 更清晰, 更推荐——看到 for (; …;) 时想想是不是 while 更合适


for语句省略表达式 (全省: 省略2)

三个表达式全省: 条件默认为真——for(;;) 是最常见的无限循环写法

#include <stdio.h>
int main(void) {
    int n;
    for (;;) {
        printf("输入一个数 (0 结束): ");
        scanf("%d", &n);
        if (n == 0)
            break;      /* 靠 break 跳出 */
        printf("%d 的平方是 %d\n", n, n * n);
    }
    return 0;
}
  • 分号不能少: for(;;) 里两个分号都在

  • 没有 break/return, 循环永远不停——试试把 break 删掉再跑, 用 Ctrl+C 打断

  • break 下页细讲; 和 5-1 的 while (1) 是同类写法, 习惯用哪个都行


C99中的for语句 (声明循环变量)

C99 里, for 的表达式1 可以换成声明——循环变量直接在 for 里声明:

#include <stdio.h>
int main(void) {
    int n = 100, sum = 0;
    for (int i = 1; i <= n; i++) {
        sum += i;
    }
    printf("1+...+%d = %d\n", n, sum);
    return 0;
}
  • 变量 i 不用提前声明, 在 for 里声明即用

  • 运行结果: 1+...+100 = 5050

  • 好处: 循环变量成了循环的"私有变量", 外面看不见也改不了


C99中的for语句 (作用域)

循环内声明的变量出了循环就用不了:

#include <stdio.h>
int main(void) {
    int n = 100, sum = 0;
    for (int i = 1; i <= n; i++) {
        sum += i;
    }
    /* printf("%d", i);  编译错误: i 出了循环不可见 */
    printf("1+...+%d = %d\n", n, sum);
    return 0;
}
  • 变量 i 的作用域只在循环内——循环结束后访问会编译报错 (把注释打开试试)

  • 如果要循环结束后还要用这个变量, 就只能在循环外声明

  • 多数情况下 for 自管循环变量更方便、可读性更好


逗号运算符

for 需要两个初始化表达式或一次递增多个变量时, 可用逗号表达式作第一或第三个表达式, 形式为:

表达式1, 表达式2

计算逗号表达式的步骤:

  • 首先计算表达式1并且丢弃计算出的值

  • 然后计算表达式2, 并把它的值作为整个表达式的值

逗号运算符的优先级低于所有其他运算符, 结合方向为左结合

例子:

i = 1, j = 2, k = i + j   /* (i=1), (j=2), (k=i+j) */

C99中的for语句 (多变量声明)

for 可以一次声明多个变量 (类型相同), 用逗号分隔:

#include <stdio.h>
int main(void) {
    int n = 5, sum = 0;
    for (int i = 1, j = 1; i <= n; i++, j += 2) {
        sum += j;
    }
    printf("前 %d 个奇数之和 = %d\n", n, sum);
    return 0;
}
  • 两个变量 i、j 都在表达式 1 声明, 表达式 3 同时递增 (i++、j += 2)

  • 运行结果: 前 5 个奇数之和 = 25 (1+3+5+7+9)

  • 表达式3 的 i++, j += 2 正是刚讲的逗号运算符——概念立刻兑现

  • 常见场景: 一个循环里需要两个同步变化的变量 (计数的 + 参与的)


程序: 打印平方表 (for 版)

现场演示: 用 for 重写上一课的平方表程序——输出一模一样

样例 (输入 5):

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

提示: while 版的三件事 (初始化、判定、递增) 怎么搬进 for 的三个表达式?


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

/* 打印平方表 (for 版) */
#include <stdio.h>

int main(void)
{
  int i, n;

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

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

  return 0;
}
  • 三件事搬进三个表达式: i = 1 → 表达式1; i <= n → 表达式2; i++ → 表达式3

  • 输出与 while 版完全一致 (输入 5 → 5 行平方表)

  • for 把三件事集中一行, 计数循环更清晰——这正是 for 的主场


嵌套循环: 打印星号塔

现场演示: 输入高度 n, 打印 n 行的居中星号塔 (第 i 行: 前面补空格, 星号 2i-1 颗)

提示: 每行 = 空格 + 星号——空格负责居中 (第 i 行 n-i 个), 星号第 i 行 2i-1 颗; 加上外层, 一共三层循环


嵌套循环: 打印星号塔 (参考代码)

/* stars.c: 打印居中星号塔 */
#include <stdio.h>

int main(void)
{
    int n;
    printf("输入高度: ");
    scanf("%d", &n);

    for (int i = 1; i <= n; i++) {          /* 外层: 控制行数 */
        for (int j = 1; j <= n - i; j++) {  /* 空格: 居中 */
            printf(" ");
        }
        for (int j = 1; j <= 2 * i - 1; j++) { /* 星号: 2i-1 颗 */
            printf("*");
        }
        printf("\n");
    }
    return 0;
}
  • 外层 i 管行数; 两个内层分别管空格和星号——三层的上限都依赖 i

  • 空格数 n - i 递减、星号数 2 * i - 1 递增——居中的数学

  • 内层上限依赖外层变量是嵌套循环的精髓; 输入 5 → 5 层金字塔


循环惯用法: 累加与累乘

累加: 计算 1+2+...+n

累乘: 计算 n! (注意: 初始值为 1, 且很快溢出)

累加器初始化为 0, 累乘器初始化为 1——这是循环的经典写法, 初值写错结果全错

int sum = 0;
for (int i = 1; i <= n; i++) {
    sum += i;
}
int product = 1;
for (int i = 1; i <= n; i++) {
    product *= i;
}

算法案例: 素数判断

现场演示: 输入一个整数, 判断它是不是素数 (只能被 1 和自己整除)

样例:

输入一个数: 13
13 是素数

提示: 检查 2 ~ n-1 里有没有约数; 找到第一个约数就可以停了——想一想用什么


算法案例: 素数判断 (参考代码)

/* primes.c: 判断 n 是否为素数 */
#include <stdio.h>

int main(void) {
    int n, d;
    printf("输入一个数: ");
    scanf("%d", &n);

    for (d = 2; d < n; d++) {
        if (n % d == 0)
            break;        /* 找到约数, 提前退出 */
    }

    if (d < n)
        printf("%d 不是素数\n", n);
    else
        printf("%d 是素数\n", n);
    return 0;
}
  • 循环两种退法: 找到约数 break (提前退出) 或跑完 2~n-1 (正常结束)——用 d < n 区分

  • 改进: 只需检查到 \(\sqrt{n}\)——左栏 divisor * divisor <= n 就是等价写法 (不用 math.h); 现场试写 sqrt 版: divisor <= sqrt(n) 对比效果


综合案例: 非完全平方数的和

现场演示: 输入 n, 计算 1~n 中所有非完全平方数的和 (完全平方数: 1、4、9、16、…)

样例 (n = 10):

输入 n: 10
非完全平方数之和 = 41

(1~10 的和 55, 减去完全平方数 1+4+9 = 41)

提示: 怎么判断一个数是不是完全平方数? (想想第 3 周的工具箱); 遇到完全平方数要跳过——用什么?


综合案例: 非完全平方数的和 (参考代码)

/* perfectsquare.c: 1~n 中非完全平方数之和 */
#include <stdio.h>
#include <math.h>

int main(void) {
    int n, sum = 0;
    printf("输入 n: ");
    scanf("%d", &n);

    for (int i = 1; i <= n; i++) {
        int r = (int) sqrt(i);
        if (r * r == i)
            continue;          /* 完全平方数, 跳过 */
        sum += i;
    }
    printf("非完全平方数之和 = %d\n", sum);
    return 0;
}
  • r = (int) sqrt(i) + r * r == i: 第 3 周的强制转换在这里兑现

  • continue 跳过完全平方数——循环与数学库函数的第一次合作


课堂小测 1 (概念)

看代码, 回答: 循环体执行了多少次?

int i;
for (i = 0; i < n; i++) {
    printf("%d ", i);
}
  • 输出会是从几到几? 一共几个数?


课堂小测 2 (概念)

看代码, 回答: 这是什么循环? 怎么停下来?

for (;;) {
    scanf("%d", &n);
    if (n == 0)
        break;
    printf("%d 的平方是 %d\n", n, n * n);
}
  • for(;;) 省略的三个表达式分别默认什么?


课堂小测 3 (编码)

用 for 求 1+2+...+n

样例 (n = 100):

1+...+100 = 5050

写完后编译运行验证


课堂小测 4 (编码)

打印乘法口诀表: 第 i 行输出 i 个式子

样例 (前 3 行):

1×1=1
1×2=2 2×2=4
1×3=3 2×3=6 3×3=9

写完后编译运行验证


课堂小测 5 (纠错)

找错: 下面代码哪里错了?

int sum = 0;
for (i = 1; i <= n; i--)
    sum += i;
  • 想想 i 的值怎么变化, 循环什么时候停?


课堂小测 6 (纠错)

找错: 想打印乘法口诀表, 下面代码哪里错了?

for (int i = 1; i <= 9; i++)
    for (int j = 1; j <= i; j++)
        printf("%d×%d=%d\n", j, i, j * i);
  • 运行一下会看到什么? 和真正的口诀表差在哪?


课堂小测 7 (分析)

分析运行结果: 下面嵌套循环输出什么?

for (int i = 1; i <= 3; i++) {
    int j = 1;
    while (j <= i) {
        printf("*");
        j++;
    }
    printf("\n");
}

课堂小测 8 (分析)

分析运行结果: count 最后是多少?

int count = 0;
for (int i = 1; i <= 3; i++)
    for (int j = 1; j <= 2; j++)
        count++;
printf("%d\n", count);