上一篇我们让程序学会了"做决策"——if、else、switch。程序终于不再是一条路走到黑了,它可以根据条件选择不同的执行路径。但这还不够。

想象你要在屏幕上打印 1 到 100 的所有数字。用目前学到的知识,你只能写 100 行 printf——这显然不是程序员该干的事。或者你想做一个游戏,让玩家反复猜数字直到猜对为止——你不可能预知玩家要猜几次,所以你没法用 if-else 来写。

这时候就需要循环——让一段代码反复执行,直到满足某个条件才停下来。C 语言提供了三种循环语句:while、for、do-while。这篇文章会逐一吃透它们,还要讲 break 和 continue 这两个控制循环流向的关键字,最后用一篇完整项目——猜数字游戏——把你前面所有知识串起来。

这篇文章还会用到几个 C 标准库中的新函数:rand()、srand()、time(),分别声明在 <stdlib.h> 和 <time.h> 中。你不用提前了解它们——我们会在用到的地方逐一展开。

while 循环——最简单的循环

while 的语法和 if 几乎一模一样:

while (表达式)
    语句;

唯一的区别:if 的条件满足时,语句只执行一次;while 的条件满足时,语句会反复执行,直到条件不满足为止。

对比一下就更清楚了:

#include <stdio.h>
 
int main()
{
    /* if 只执行一次 */
    if (1)
        printf("hehe\n");    /* 打印 1 次 */
 
    /* while 反复执行,直到条件为假 */
    /* 注意:这里条件永远是 1(真),会死循环! */
    /* while (1)
        printf("hehe\n");   打印无数次 */
 
    return 0;
}

while 的执行流程:先判断条件 → 为真就执行循环体 → 执行完再判断 → 还为真就再执行 → ... → 直到条件为假才停止。如果你不小心把条件写成了永远为真的表达式(比如 while(1)),程序就会"死循环"——一直跑、停不下来,只能强制关闭。

任何循环都有三个要素:初始化、判断、调整。来看打印 1 到 10 的例子:

#include <stdio.h>
 
int main()
{
    int i = 1;                    /* 初始化:循环变量从 1 开始 */
 
    while (i <= 10)               /* 判断:当 i <= 10 时进入循环 */
    {
        printf("%d ", i);         /* 执行:打印当前的 i */
        i = i + 1;                /* 调整:i 自增 1,准备下一次循环 */
    }
    printf("\n");
 
    return 0;
}
/* 输出:1 2 3 4 5 6 7 8 9 10 */

while 的三要素分散在不同位置:初始化在循环前面,判断在 while 括号里,调整在循环体内部。这正是 while 的特点——也是它的弱点:代码多了之后,调整部分可能被遗漏或写错位置。

while 的经典坑:忘记写调整语句

看这段"事故代码":

#include <stdio.h>
 
int main()
{
    int i = 1;
 
    while (i <= 10)
    {
        printf("%d ", i);     /* 忘了写 i++! */
    }
 
    return 0;
}
/* 输出:1 1 1 1 1 1 ...(死循环!) */

i 永远不增长,条件永远为真,程序永远停不下来。这是 while 循环最容易犯的错误——写了判断,忘了调整。for 循环为什么更常用?就是因为它把三个要素集中在一行,不容易漏。

实战:逆序打印一个整数的每一位

再来看一个更巧妙的例子——逆序打印一个整数的每一位。输入 1234,输出 4 3 2 1:

#include <stdio.h>
 
int main()
{
    int n = 0;
    scanf("%d", &n);              /* 例:输入 1234 */
 
    while (n)                     /* n 不为 0 就继续 */
    {
        printf("%d ", n % 10);    /* n%10 得到最低位:4, 3, 2, 1 */
        n /= 10;                  /* n=n/10,去掉最低位:123, 12, 1, 0 */
    }
    printf("\n");
 
    return 0;
}
/* 输入 1234 → 输出 4 3 2 1 */

这个例子的精妙之处在于利用了整数的特性:% 10 能得到最低位,/ 10 能去掉最低位。反复这两个操作,就能把每一位都剥出来。而且 while (n) 里的 n 既是循环变量又是判断条件——当 n 变成 0 时,循环自动结束。

注意一个边界问题:如果输入的是 0,while (0) 条件为假,循环一次都不执行,什么都不输出。这在后面用 do-while 时会形成鲜明对比——do-while 正是为了处理这种"至少要执行一次"的场景。

for 循环——最常用的循环

for 是三种循环中使用频率最高的。它的语法把初始化、判断、调整三个要素集中在了同一行:

for (表达式1; 表达式2; 表达式3)
    语句;
  • 表达式1:初始化,只在循环开始前执行一次
  • 表达式2:判断条件,每次循环前检查,为假则退出
  • 表达式3:调整,每次循环体执行完后执行

执行顺序:表达式1(一次)→ 表达式2(真)→ 循环体 → 表达式3 → 表达式2(真)→ 循环体 → ...

用 for 打印 1 到 10:

#include <stdio.h>
 
int main()
{
    int i = 0;
 
    for (i = 1; i <= 10; i++)     /* 初始化 i=1, 判断 i<=10, 调整 i++ */
    {
        printf("%d ", i);         /* 循环体:打印 i */
    }
    printf("\n");
 
    return 0;
}

和 while 版本对比,for 的三要素在同一行,一目了然。当代码量增大时,for 的可维护性明显更好。

for 与 while 的等价转换

任何 for 循环都可以改写成 while,反之亦然:

/* for 版 */
for (i = 1; i <= 10; i++)
{
    printf("%d ", i);
}
 
/* 等价的 while 版 */
i = 1;                /* 初始化 */
while (i <= 10)       /* 判断 */
{
    printf("%d ", i);
    i++;              /* 调整 */
}

它们只是"把三要素写在哪儿"的区别,底层行为完全一样。记住这个等价关系,你就不会在"用 for 还是 while"上纠结——已知循环次数用 for,次数未知用 while。

for 循环的三个表达式都可以省略

for 循环的三个表达式都可以省略(但分号不能省):

  • 省略表达式1:在外面初始化
  • 省略表达式2:条件永远为真 → 死循环,除非用 break 跳出
  • 省略表达式3:在循环体内调整
/* 省略所有表达式——死循环 */
for (;;)
{
    /* 需要 break 或 return 来跳出 */
}

for (;;) 是一种"经典死循环"写法,等价于 while (1)。两者在几乎所有编译器下生成的机器码都一样,用哪个纯属个人喜好。看到 for (;;) 不要慌,它只是"无限循环"的意思。

实战:计算 1 到 100 之间 3 的倍数的和

#include <stdio.h>
 
int main()
{
    int i = 0;
    int sum = 0;
 
    /* 方法1:遍历所有数,判断是否能被 3 整除 */
    for (i = 1; i <= 100; i++)
    {
        if (i % 3 == 0)
            sum += i;
    }
    printf("3 的倍数之和(方法1)= %d\n", sum);  /* 1683 */
 
    /* 方法2:直接从 3 开始,每次加 3,效率更高 */
    sum = 0;
    for (i = 3; i <= 100; i += 3)
    {
        sum += i;
    }
    printf("3 的倍数之和(方法2)= %d\n", sum);  /* 1683 */
 
    return 0;
}

方法 2 不需要判断 i % 3 == 0,循环次数也从 100 次减少到 33 次——这是写循环时应该培养的优化意识:让循环变量直接跳过不相关的值。

for 循环的常见错误

错误一:分号位置写错。 for (i = 1; i <= 10; i++); 在 ) 后面多了一个分号——这个分号本身就是空语句,成了循环体。结果是循环"空转"10 次,然后执行下面那条 printf 一次。这是最隐蔽的 bug 之一:

#include <stdio.h>
 
int main()
{
    int i = 0;
 
    for (i = 1; i <= 10; i++);   /* 注意这个分号!循环体是空语句 */
    {
        printf("%d ", i);        /* 只执行一次,输出 11 */
    }
 
    return 0;
}

错误二:循环变量在循环体里被意外修改。 比如有人图省事在循环体内又写 i++,循环次数立刻减半且难以察觉。循环变量的调整只应该出现在表达式 3 或循环体末尾。

错误三:<= 和 < 用错导致多一次或少一次。 for (i = 0; i < n; i++) 和 for (i = 0; i <= n; i++) 差一次。习惯上,C 程序员用 i < n 表示"循环 n 次"(从 0 到 n-1),这样和数组下标 0~n-1 天然对齐,以后学数组你就懂了。

do-while 循环——先斩后奏

while 和 for 都是"先判断,再执行"——条件不满足就一次都不执行。但有些场景下,你希望"至少执行一次,再判断要不要继续"。do-while 就是为这种场景设计的:

do
    语句;
while (表达式);

注意:while (表达式); 后面有个分号!这个分号很容易忘。

执行流程:先执行一次循环体 → 再判断条件 → 为真则继续循环。

对比 while 和 do-while:

#include <stdio.h>
 
int main()
{
    int i = 100;
 
    /* while:先判断,条件不满足,一次都不执行 */
    while (i <= 10)
    {
        printf("%d ", i);
        i++;
    }
    /* 什么都不输出 */
 
    /* do-while:先执行一次,再判断 */
    i = 100;
    do
    {
        printf("%d ", i);    /* 输出 100,即使条件不满足也执行了一次 */
        i++;
    } while (i <= 10);
    /* 输出 100 */
 
    return 0;
}

实战:统计一个正整数的位数

#include <stdio.h>
 
int main()
{
    int n = 0;
    scanf("%d", &n);           /* 例:输入 1234 */
 
    int cnt = 0;               /* 计数器 */
 
    do
    {
        cnt++;                 /* 每去掉一位,计数加 1 */
        n = n / 10;            /* 去掉最低位 */
    } while (n);               /* n 不为 0 就继续 */
 
    printf("位数 = %d\n", cnt); /* 输出 4 */
 
    return 0;
}

这个场景特别适合 do-while:即使输入的是 0,也应该输出"1 位数"。如果用 while,输入 0 时 n 为 0,条件不满足,一次都不执行,cnt 还是 0——这显然不对。

do-while 的真实使用场景

do-while 用得最少,但有一个场景它几乎是唯一选择:菜单交互。用户至少要看到一次菜单,选择退出后才结束——"先显示,后判断"正好对应 do-while。猜数字游戏的主循环就是经典例子,后面你马上就能看到。

break 和 continue——控制循环的两个开关

break 和 continue 用于在循环体内改变执行流程:

  • break:永久终止循环。一旦执行到 break,直接从循环中跳出来。
  • continue:跳过本次循环的剩余部分,直接进入下一次循环的条件判断(或 for 的调整部分)。

while 中的 break

#include <stdio.h>
 
int main()
{
    int i = 1;
 
    while (i <= 10)
    {
        if (i == 5)
            break;            /* 当 i=5 时,永久跳出循环 */
 
        printf("%d ", i);     /* 只打印了 1 2 3 4 */
        i++;
    }
    /* 输出:1 2 3 4 */
 
    return 0;
}

while 中的 continue——小心死循环!

#include <stdio.h>
 
int main()
{
    int i = 1;
 
    while (i <= 10)
    {
        if (i == 5)
            continue;         /* 跳过 i=5 这次循环的剩余部分 */
 
        printf("%d ", i);
        i++;                  /* 注意:continue 导致 i++ 被跳过! */
    }
    /* 当 i=5 时,continue 跳过了 i++,i 永远是 5 → 死循环! */
 
    return 0;
}

这是一个非常重要的陷阱! 在 while 循环中,如果 continue 跳过了循环变量的调整语句,就会导致死循环。而 for 循环不会有这个问题——因为 for 的调整部分(表达式3)在 continue 之后依然会执行。

为什么会这样?回到执行流程:while 里 continue 的作用是"跳到循环体的结束处",然后去执行条件判断——调整语句如果在 continue 之后,就被跳过了。而 for 的表达式 3 在语法上属于"循环控制部分",continue 跳到的是表达式 3 之前,所以调整照常执行。记住这个本质区别,比死记结论更有用。

for 中的 continue

#include <stdio.h>
 
int main()
{
    int i = 0;
 
    for (i = 1; i <= 10; i++)
    {
        if (i == 5)
            continue;             /* 跳过本次打印,但 i++ 仍会执行! */
 
        printf("%d ", i);         /* 打印 1 2 3 4 6 7 8 9 10 */
    }
 
    return 0;
}
/* 输出:1 2 3 4 6 7 8 9 10 (跳过了 5) */

总结:while 中 continue 容易造成死循环(因为跳过了调整语句),for 中 continue 安全得多(调整在头部,不受 continue 影响)。

do-while 里的 break 和 continue 行为与 while 中完全一致:break 直接终止循环,continue 跳过本次循环体剩余代码,去执行 while (表达式) 的条件判断——如果循环变量的调整写在 continue 后面,同样可能陷入死循环,需要注意。

break 和 continue 的适用范围

一个容易混淆的点:break 只能跳出"最近的一层"循环或 switch,continue 只能作用于它所在的循环。如果你有三层循环嵌套,在第三层写 break,它只跳出第三层,外层循环继续跑。想要一次跳出多层?后面讲的 goto 是一个方案,也可以用"标志变量"配合多层判断。

另外注意:continue 在 switch 里是非法的(除非 switch 在循环里)。continue 只用于循环,break 既用于循环也用于 switch。

循环嵌套——循环里面套循环

有时候一层循环不够用。比如打印 9×9 乘法表,你需要外层循环控制行,内层循环控制列。

实战 1:打印 9×9 乘法表

#include <stdio.h>
 
int main()
{
    int i = 0, j = 0;
 
    for (i = 1; i <= 9; i++)          /* 外层:控制行 */
    {
        for (j = 1; j <= i; j++)      /* 内层:控制列,每行打印 i 个 */
        {
            printf("%d*%d=%-2d ", j, i, i * j);
            /* %-2d:左对齐,宽度 2,让表格整齐 */
        }
        printf("\n");                 /* 一行结束换行 */
    }
 
    return 0;
}
/* 输出:
 * 1*1=1
 * 1*2=2  2*2=4
 * 1*3=3  2*3=6  3*3=9
 * ...(完整的 9 行)*/

分析这个例子的执行过程:外层 i=1 时,内层 j 从 1 到 1 循环一次;外层 i=2 时,内层 j 从 1 到 2 循环两次……外层每执行一次,内层完整地执行一整轮。这就是嵌套循环的节奏——像时钟的时针和分针,分针(内层)转一圈,时针(外层)才走一格。

实战 2:找出 100 到 200 之间的所有素数

素数(质数)的定义是:只能被 1 和它本身整除的数。判断一个数 i 是不是素数,需要用 2 到 i-1 之间的每个数去试除:

#include <stdio.h>
 
int main()
{
    int i = 0, j = 0;
 
    for (i = 100; i <= 200; i++)          /* 外层:遍历 100~200 */
    {
        int flag = 1;                       /* 假设 i 是素数 */
 
        for (j = 2; j < i; j++)            /* 内层:用 2~i-1 试除 */
        {
            if (i % j == 0)                /* 如果 j 能整除 i */
            {
                flag = 0;                  /* i 不是素数 */
                break;                     /* 不用继续试了 */
            }
        }
 
        if (flag == 1)                     /* 如果 flag 还是 1 */
            printf("%d ", i);              /* i 是素数,打印出来 */
    }
    printf("\n");
 
    return 0;
}

两层嵌套:外层 for 产生 100 到 200 的每个数,内层 for 对每个数进行素数判断。flag 是一个"标记变量"——初始设为 1(假设是素数),一旦找到因子就改为 0。这种"先假设为真,再寻找反例"的思路在编程中非常常见。

两个可以优化的点(现在知道即可,以后会深入):

  1. 试除范围从 j < i 缩小到 j <= sqrt(i)——因为如果 i 有因子,必有一个不超过它的平方根;
  2. 偶数(除了 2)不可能是素数,外层可以直接跳过偶数。

优化后的内层循环次数从 i 次降到 √i 次,100~200 区间整体耗时下降一个数量级。写循环时先想"能不能少转几圈",这是性能意识的起点。

实战 3:打印直角三角形星号

#include <stdio.h>
 
int main()
{
    int i = 0, j = 0;
    int n = 5;
 
    for (i = 1; i <= n; i++)          /* 控制行数 */
    {
        for (j = 1; j <= i; j++)      /* 控制每行星号个数 */
        {
            printf("*");
        }
        printf("\n");
    }
 
    return 0;
}
/* 输出:
 * *
 * **
 * ***
 * ****
 * *****
 */

把内层条件改成 j <= n - i + 1,就能得到倒三角。改成"先打空格再打星号"就能得到金字塔——这些都是锻炼"循环变量关系"的好练习,强烈建议自己动手试一遍。

嵌套循环的总执行次数估算

一个"外层 N 次 × 内层 M 次"的嵌套循环,总执行次数是 N×M 次。三层嵌套就是 N×M×K。嵌套深度每加一层,执行次数就多乘一个维度——这也是为什么嵌套太深(超过 3 层)的代码既难读又可能慢。以后学时间复杂度时你会用 O(N²)、O(N³) 来精确描述它。

goto 语句——谨慎使用的"任意门"

goto 可以让你在同一个函数内跳转到任意一个标号处:

#include <stdio.h>
 
int main()
{
    printf("hehe\n");
    goto next;                     /* 直接跳到 next: 标号处 */
    printf("haha\n");              /* 这行永远不会执行 */
 
next:
    printf("跳过了 haha 的打印\n");
 
    return 0;
}
/* 输出:
 * hehe
 * 跳过了 haha 的打印
 */

大多数情况下,goto 会让代码变得难以理解和维护——你永远不知道程序"从哪里跳过来的"。所以建议是:能不用的地方尽量不用。C 语言"结构化编程"的思想核心就是:只用顺序、选择、循环三种结构,就可以表达所有算法,完全不需要 goto。Dijkstra 早在 1968 年就写过著名的《Go To Statement Considered Harmful》来批判它。

但 goto 有一个很实用的场景:跳出多层嵌套循环。break 只能跳出当前这一层,如果你嵌套了三层,想一次性全部跳出,goto 就很方便:

for (...)
{
    for (...)
    {
        for (...)
        {
            if (disaster)
                goto error;     /* 一次性跳出所有三层循环 */
        }
    }
}
error:
// 处理错误...

不用 goto 的话,你需要设置多个标记变量并在每一层都判断,代码会臃肿很多。

goto 的使用纪律

如果决定用 goto,遵守三条纪律:

  1. 只能向后跳(跳到后面的标号),不要向前跳——向前跳可能跳过变量的初始化,是未定义行为;
  2. **只用于"从深层结构统一跳出/退出"**这一种场景,不用于实现循环(那会写出"意大利面条"代码);
  3. 标号附近加注释,说明为什么用 goto 而不是其他方式。

猜数字游戏——知识大综合

前面学到的所有知识,现在来一个大综合。我们要写一个完整的猜数字游戏:

  • 电脑随机生成 1~100 之间的一个数字
  • 玩家输入猜测的数字
  • 每次猜完告诉玩家"猜大了"还是"猜小了"
  • 猜对就结束,显示恭喜信息
  • 玩家可以反复玩,直到选择退出

第一步:生成随机数

C 语言用 rand() 函数生成随机数,它返回 0 到 RAND_MAX(通常是 32767)之间的整数。rand() 声明在 <stdlib.h> 中。

#include <stdio.h>
#include <stdlib.h>    /* rand() 和 srand() 在这里 */
 
int main()
{
    printf("%d\n", rand());
    printf("%d\n", rand());
    printf("%d\n", rand());
    printf("%d\n", rand());
    printf("%d\n", rand());
 
    return 0;
}

运行一次看看结果,再运行一次——你会发现两次生成的"随机数"完全一样!

这是因为 rand() 生成的是伪随机数:它不是真正的随机,而是通过一个"种子"值经过数学公式算出来的。默认种子是 1,所以每次运行都得到一样的序列。

为什么是"伪随机"?因为计算机本质上是确定性的机器——同样的输入必然产生同样的输出。真正的随机性需要借助外部物理现象(放射性衰变、热噪声等),普通程序无法直接获得。所以 rand() 采用"线性同余法"之类的数学公式:下一个数 = (a × 当前数 + c) mod m,只要种子相同,后面的序列就完全一样。这在"可复现性"上有好处(调试方便),但在"不可预测性"上是失败的——所以它绝不能用于密码学场景(那要用 rand_s 或操作系统提供的加密随机源)。

要让种子变化,用 srand() 设置种子,再用 time() 获取当前时间(时间戳)作为种子:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>      /* time() 在这里 */
 
int main()
{
    /* 用当前时间(不断变化)作为随机种子 */
    srand((unsigned int)time(NULL));
    /* time(NULL) 返回从 1970-01-01 到现在的秒数,每次都不一样 */
 
    printf("%d\n", rand());
    printf("%d\n", rand());
    printf("%d\n", rand());
 
    return 0;
}
/* 每次运行,结果都不同! */

注意:srand() 在程序中只需要调用一次,放在 main 函数开头就行,不要放在循环里——否则每次循环都重新以当前时间设种子,而秒级的时间在瞬间的循环中是不变的,随机性反而更差。一次性播种,反复取数才是正确姿势。

第二步:控制随机数范围

rand() 返回的是 0RAND_MAX 的大范围值,我们需要把它限制在 1100:

rand() % 100;         /* 余数范围 0~99 */
rand() % 100 + 1;     /* 余数范围 1~100 */

通用的产生 a~b 之间随机数的公式:

a + rand() % (b - a + 1)

解释一下原理:rand() % (b - a + 1) 把结果压到 0 ~ (b-a) 之间,再加上 a,就得到 a ~ b。比如要 1~100:1 + rand() % 100。

一个专业提醒:% 取余法有一个理论缺陷——如果 b-a+1 不能整除 RAND_MAX+1,各余数的出现概率略有不均(低余数出现的概率稍高)。对游戏来说完全够用,但对需要严格均匀分布的场合(比如抽样),应该用更专业的做法(如拒绝采样)。现在知道有这个说法就行。

第三步:完整游戏

#include <stdio.h>
#include <stdlib.h>   /* rand(), srand() */
#include <time.h>     /* time() */
 
void menu()           /* 显示菜单的函数 */
{
    printf("***********************\n");
    printf("******  1. play  ******\n");
    printf("******  0. exit  ******\n");
    printf("***********************\n");
}
 
void game()           /* 玩一局游戏的函数 */
{
    int r = rand() % 100 + 1;   /* 生成 1~100 的随机数 */
    int guess = 0;              /* 玩家猜的数字 */
 
    while (1)                   /* 死循环,猜对才退出 */
    {
        printf("请猜数字(1~100): ");
        scanf("%d", &guess);
 
        if (guess < r)
            printf("猜小了\n");
        else if (guess > r)
            printf("猜大了\n");
        else
        {
            printf("恭喜你,猜对了!\n");
            break;              /* 猜对,跳出循环 */
        }
    }
}
 
int main()
{
    int input = 0;              /* 用户菜单选择 */
 
    srand((unsigned int)time(NULL));  /* 设置随机种子,只执行一次 */
 
    do
    {
        menu();                 /* 显示菜单 */
        printf("请选择: ");
        scanf("%d", &input);
 
        switch (input)
        {
            case 1:
                game();         /* 开始游戏 */
                break;
            case 0:
                printf("游戏结束\n");
                break;
            default:
                printf("选择错误,请重新选择\n");
                break;
        }
    } while (input);            /* input 为 0 时退出循环 */
 
    return 0;
}

来拆解这个程序的结构,你会发现它用上了前面所有的知识:

  • do-while 做菜单循环:至少显示一次菜单,输入 0 退出——这正是上一篇讲的 do-while 的典型场景
  • switch 处理菜单分支:1 玩游戏、0 退出、其他提示重选
  • while (1) + break 做游戏循环:猜对才跳出
  • 函数封装:menu() 和 game() 各管一件事,main 只做总调度

while (1) 会不会死循环?不会——因为循环体里一定有 break 保证出口。while (1) 本身不可怕,可怕的是循环体里没有任何退出路径。

第四步:加上次数限制

再给游戏加一个功能:玩家只有 5 次猜测机会,5 次猜不中就失败:

#include <stdio.h>
#include <stdlib.h>
#include <time.h>
 
void menu()
{
    printf("***********************\n");
    printf("******  1. play  ******\n");
    printf("******  0. exit  ******\n");
    printf("***********************\n");
}
 
void game()
{
    int r = rand() % 100 + 1;   /* 正确答案 */
    int guess = 0;              /* 玩家猜测 */
    int count = 5;              /* 剩余次数 */
 
    while (count)               /* count 不为 0 就继续 */
    {
        printf("\n你还有 %d 次机会\n", count);
        printf("请猜数字: ");
        scanf("%d", &guess);
 
        if (guess < r)
            printf("猜小了\n");
        else if (guess > r)
            printf("猜大了\n");
        else
        {
            printf("恭喜你,猜对了!\n");
            break;              /* 猜对了,不用继续 */
        }
 
        count--;                /* 每次猜完,次数减 1 */
    }
 
    if (count == 0)             /* 5 次用完,失败了 */
        printf("你失败了!正确值是: %d\n", r);
}
 
int main()
{
    int input = 0;
 
    srand((unsigned int)time(NULL));
 
    do
    {
        menu();
        printf("请选择: ");
        scanf("%d", &input);
 
        switch (input)
        {
            case 1:
                game();
                break;
            case 0:
                printf("游戏结束\n");
                break;
            default:
                printf("选择错误,重新选择\n");
                break;
        }
    } while (input);
 
    return 0;
}

这里有个细节:while (count) 利用"非 0 即真"——count 从 5 递减到 0,0 为假循环结束。循环结束后用 if (count == 0) 判断是"猜对退出"还是"次数耗尽退出"(因为猜对时 count 还没减到 0)。用循环退出后的状态反推退出原因,是一个常用技巧。

改进方向(自己动手试一试)

  1. 校验输入:玩家输入 150 或 -3 时给出提示,不消耗次数——用 if (guess < 1 || guess > 100) 拦截;
  2. 记录次数:猜对时打印"你一共猜了 X 次";
  3. 难度选择:菜单里加"1.简单(150) 2.中等(1100) 3.困难(1~200)";
  4. 清屏:Windows 下用 system("cls")(需要 <stdlib.h>)在每局开始前清屏;
  5. 处理非法输入:scanf 读到非数字时会失败,此时要清空输入缓冲区(while (getchar() != '\n');)并重新提示。

猜数字游戏里的循环总结

这个游戏几乎用到了我们前面所有文章的知识点:数据类型、变量初始化、scanf/printf、if-else 条件判断、switch 分支、while 循环、do-while 循环、break 跳出、函数定义与调用(menu() 和 game())、随机数生成(rand/srand/time)。你现在已经有能力独立写出这样的程序了。

三种循环怎么选?

一个简单的原则:循环次数已知用 for(如遍历数组、打印 1~N);循环次数未知、先判断再进入用 while(如读取文件直到 EOF);至少需要执行一次用 do-while(如输入验证、菜单交互)。实际开发中 for 和 while 用得最多,do-while 只在特定场景下使用。

循环判断时机至少执行次数典型场景
while先判断0 次读文件直到结束、条件驱动的循环
for先判断0 次已知次数的遍历、计数循环
do-while后判断1 次菜单交互、输入校验

还有一个经验之谈:循环写完之后,先"走一遍边界"。分别用"第一次进入""最后一次进入""刚好不满足条件"三个值在心里模拟一遍,很多 off-by-one(差一)错误当场就能发现。这是写循环最重要的自检习惯。


本篇思考题

  1. 下面的循环各执行几次?for (i = 0; i < 10; i++)、for (i = 0; i <= 10; i++)、for (i = 10; i > 0; i--)?
  2. 为什么 while 循环里 continue 可能造成死循环,而 for 不会?从两者的执行流程解释。
  3. 输入一个整数 n,用 for 计算 1~n 的累加和;再用 while 写一遍,对比两种写法。
  4. 用嵌套循环打印一个 5 层的金字塔(第 i 行有 2i-1 个星号,居中排列)。
  5. for (;;) 和 while (1) 是什么关系?它们的退出条件靠什么提供?
  6. rand() % 100 + 1 的取值范围是什么?如果要生成 50~100 的随机数怎么写?
  7. 猜数字游戏里,如果玩家输入的不是数字(比如 abc),scanf 会怎样?怎么处理?

思考题参考答案与详解

第 1 题:

  • for (i = 0; i < 10; i++):执行 10 次(i 取 0~9);
  • for (i = 0; i <= 10; i++):执行 11 次(i 取 0~10);
  • for (i = 10; i > 0; i--):执行 10 次(i 取 10~1)。

牢记 i < n 表示"循环 n 次",i <= n 表示"循环 n+1 次"——这是最常见的差一(off-by-one)错误来源。

第 2 题:因为两者的执行流程不同。while 里 continue 的作用是跳到循环体的末尾,随后去执行条件判断;如果循环变量的"调整"语句(如 i++)写在循环体末尾、位于 continue 之后,就被跳过了,循环变量永远不变、条件永远为真 → 死循环。而 for 的 continue 跳转到的是表达式 3(调整部分)——它在语法上属于循环控制部分,与循环体语句独立,不受 continue 影响,所以调整照常执行、循环变量正常推进,一般不产生死循环。

第 3 题:

#include <stdio.h>
int main()
{
    int n = 0, i = 0, sum = 0;
    scanf("%d", &n);
    /* for 版:初始化/判断/调整三要素集中在一行 */
    for (i = 1; i <= n; i++)
        sum += i;
    printf("sum = %d\n", sum);
 
    /* while 版:三要素分散在不同位置 */
    sum = 0;
    i = 1;
    while (i <= n)
    {
        sum += i;
        i++;
    }
    printf("sum = %d\n", sum);
    return 0;
}

两种写法结果相同。区别在于:for 的三要素集中,结构紧凑、不容易漏调整语句;while 的调整语句在循环体末尾,代码一多容易遗漏或写错位置。循环次数已知时优先用 for。

第 4 题:5 层金字塔,第 i 行(i=1~5)有 2*i-1 个星号,每行前面补 5-i 个空格即可居中对齐:

#include <stdio.h>
int main()
{
    int i = 0, j = 0;
    for (i = 1; i <= 5; i++)
    {
        for (j = 1; j <= 5 - i; j++)      /* 先打空格 */
            printf(" ");
        for (j = 1; j <= 2 * i - 1; j++)  /* 再打星号 */
            printf("*");
        printf("\n");
    }
    return 0;
}

第 5 题:for (;;) 和 while (1) 都表示无限循环(for 的三个表达式都可省略,省略表达式 2 时默认恒真),二者在几乎所有编译器下生成的机器码完全一样,可互相替换。它们的退出条件不由循环语法提供,而是靠循环体内部的 break、return 等语句在满足条件时主动跳出——所以写无限循环时必须保证循环体里有出口,否则程序会真的"永远跑下去"。

第 6 题:rand() % 100 + 1 的取值范围是 1~100(rand()%100 得到 099,加 1 后为 1100)。要生成 50100 的随机数,代入通用公式 a + rand() % (b - a + 1):b-a+1 = 100-50+1 = 51,所以写 50 + rand() % 51(余数范围 050,加 50 后为 50~100)。

第 7 题:当玩家输入 abc 时,scanf("%d", &guess) 的 %d 遇到非数字字符 a 就匹配失败,scanf 返回 0(没读到任何变量),而 guess 保持之前的值(可能是旧值或未初始化的垃圾值)。若不加处理,程序会拿着这个错误值去和正确答案比较,可能误判;而且 abc 残留在输入缓冲区,下一次循环读入仍会失败。处理方法:先检查 scanf 的返回值,不是 1 就说明输入非法,此时不消耗猜测次数,并清空缓冲区、重新提示:

int ret = scanf("%d", &guess);
if (ret != 1)                 /* 输入的不是一个整数 */
{
    while (getchar() != '\n');   /* 把缓冲区里的非法字符全部清掉 */
    printf("请输入一个数字!\n");
    continue;                 /* 跳过后面的比较与 count-- */
}

小结

这个猜数字游戏几乎用到了我们前面所有文章的知识点:数据类型、变量初始化、scanf/printf、if-else 条件判断、switch 分支、while 循环、do-while 循环、break 跳出、函数定义与调用(menu() 和 game())、随机数生成(rand/srand/time)。你现在已经有能力独立写出这样的程序了。

三种循环怎么选?一个简单的原则:循环次数已知用 for(如遍历数组、打印 1~N);循环次数未知、先判断再进入用 while(如读取文件直到 EOF);至少需要执行一次用 do-while(如输入验证、菜单交互)。实际开发中 for 和 while 用得最多,do-while 只在特定场景下使用。

随着这篇文章结束,C 语言第一阶段的核心语法也告一段落。下一篇我们将进入一个全新的维度——数组:怎么在连续的内存空间中批量管理数据,以及数组在底层到底是怎么存储的。