第6章:C 控制语句:循环
循环把重复任务写成一套规则。设计时需要明确:从什么状态开始,在什么条件下继续,每轮怎样推进,结束后留下什么结果。
本章把 while、for 和 do while 放在一起理解,并进一步用循环处理数组和实现带返回值的函数。
1. 先区分两种重复任务
| 任务类型 | 特点 | 例子 |
|---|---|---|
| 计数循环 | 次数或索引范围明确 | 依次处理数组中的四个元素 |
| 不确定次数循环 | 次数由输入或运行状态决定 | 不断读数,直到读取失败 |
“不确定次数”不等于没有结束条件,只是执行前不知道到底会重复多少次。
如果已知初始化、边界和步进,for 往往更容易表达;如果主要依据输入成功与否等条件推进,while 往往更自然。
2. while:先判断,再执行
2.1 基本结构
int count = 0;
while (count < 3) {
printf("%d\n", count);
++count;
}
输出 0、1、2,退出后 count 为 3。
初始化
↓
判断条件 ←────────┐
├─ 假 → 离开 │
└─ 真 → 循环体 │
↓ │
更新状态 ─┘
while 控制紧随其后的一条语句。要重复多个步骤,应使用花括号构成复合语句。
2.2 条件只在执行到判断处时检查
int count = 0;
while (count < 2) {
++count;
printf("count: %d\n", count);
}
第二轮中,count 增加到 2 后,仍会执行本轮后面的输出。程序回到条件处,发现 count < 2 为假,才结束循环。
循环不会因为某个变量刚发生变化,就自动中断当前循环体。
2.3 空语句与花括号
while (count < 3);
这里的循环体是一个空语句。若条件不会变化,程序可能一直停在这个循环中。
有时空循环体是有意设计的,例如工作已全部在条件表达式里完成。此时应把空语句单独放一行并说明用途,避免与误写分号混淆。
循环体的范围由语法决定,缩进只帮助阅读。遗漏花括号,可能让负责更新变量的语句实际位于循环外。
3. 通过输入状态控制循环
3.1 数据与读取状态是不同的值
long number = 0;
int status = scanf("%ld", &number);
number接收用户输入的数值。status接收成功赋值的项目数,或输入失败状态。
用户输入 0 时,如果读取成功,status 仍是 1。不能把数据为零与读取失败混为一谈。
3.2 把读取与判断合并
#include <stdio.h>
int main(void)
{
long number = 0;
long total = 0;
printf("Enter integers; a non-number ends input:\n");
while (scanf("%ld", &number) == 1) {
total += number;
}
printf("total: %ld\n", total);
return 0;
}
输入 8 -3 0 12 q,得到 total: 17。这里假定输入及累计结果都能由 long 表示。
每次进入循环体前都重新调用 scanf,只有成功读取一项才处理该值。这避免了循环前后重复编写读取代码。
3.3 为什么必须明确比较返回值
不能把条件简单写成 while (scanf("%ld", &number))。
匹配失败可能返回 0,但一开始遇到输入结束或读取错误可能返回负值 EOF。负值也是真,直接把返回值当布尔条件,会把这种输入失败当成继续执行的依据。
明确写 == 1,才表达“恰好成功读取一项”。读取两项时,则根据任务检查是否等于 2。
匹配失败的字符可能留在输入流中。上面的程序选择结束;如果想继续读取,需要另行处理不匹配的内容,不能只是再次执行完全相同的读取。
4. 条件表达式与真值
4.1 比较运算符
| 运算符 | 含义 |
|---|---|
< |
小于 |
<= |
小于或等于 |
> |
大于 |
>= |
大于或等于 |
== |
等于 |
!= |
不等于 |
这些比较产生 int 类型的 0 或 1:成立为 1,不成立为 0。
但作为控制条件的数值不必本身就是 1。零为假,非零为真,因此负数也为真。
int remaining = 3;
while (remaining) {
printf("%d\n", remaining);
--remaining;
}
对于这里的整数条件,while (remaining) 相当于 while (remaining != 0)。如果任务只接受正数,则应明确写 remaining > 0。
4.2 = 与 ==
status == 1 检查是否相等;status = 1 则把变量改成 1,并产生值 1。
因此,误写 while (status = 1) 会使判断始终为真。编译器可能警告,但这是合法的赋值表达式,不能指望语法检查自动拒绝它。
4.3 优先级与括号
算术运算通常先于关系比较,关系比较又先于赋值:
x > y + 2 → x > (y + 2)
flag = x > y → flag = (x > y)
< <= > >= 的优先级高于 == !=。连续比较并不会自动变成数学区间判断,应分别写出完整条件,再用逻辑运算符组合;第七章会详细展开。
字符可以按数值编码比较,但 name1 == name2 不能用来判断两段字符串的内容是否相同。
4.4 用布尔变量表达状态
#include <stdbool.h>
bool input_ok = (scanf("%ld", &number) == 1);
在 C99/C11 中,<stdbool.h> 提供 bool、true、false 的便利名称。bool 对应 _Bool,转换后保存 0 或 1。
input_ok、finished 等名称能表达状态含义,适合记录一个需要在多处使用的判断结果。若判断只用一次,直接写在循环条件中也很清楚。
5. 浮点比较与循环边界
5.1 先决定比较的是“完全相同”还是“足够接近”
浮点计算可能发生舍入。若算法依赖多次累加小数后恰好等于目标值,循环可能不按预期结束。
for (int step = 0; step < 10; ++step) {
double value = step / 10.0;
printf("%.1f\n", value);
}
如果需要十个采样点,用整数控制次数,再计算对应浮点值,比依赖浮点变量精确到达某个终点更容易检查。
这不意味着浮点数永远不能使用 ==。是否要求精确相等,应由数据来源和算法语义决定。
5.2 容差比较应与问题尺度相配
对于限定范围内的有限数值,若任务允许绝对误差为 0.001,可以写:
fabs(actual - expected) <= 0.001
fabs 声明在 <math.h> 中,返回浮点绝对值。这个判断表示“差值不超过允许范围”。
容差需要根据单位、数量级和任务要求选择,不能把一个固定小数或 DBL_EPSILON 当成所有问题通用的阈值。涉及无穷大、NaN 等特殊值时,还要另行判断。
6. for:集中表达初始化、判断和更新
6.1 执行顺序
for (int index = 0; index < 4; ++index) {
printf("%d\n", index);
}
输出 0、1、2、3。
初始化,只执行一次
↓
判断条件 ←────────────┐
├─ 假 → 离开 │
└─ 真 → 循环体 │
↓ │
更新表达式 ─┘
正常完成一轮循环体后,先执行更新,再进行下一次判断。第一次判断之前不执行更新。
若没有提前跳出,循环体执行四次,条件通常判断五次。最后一次判断用于发现循环应结束。
C99/C11 允许在初始化部分声明变量。这里的 index 作用域包括 for 语句,循环外不能继续使用这个局部名称。
6.2 半开区间更适合数组下标
for (int index = 0; index < count; ++index) {
/* 处理下标 index */
}
当 count 为非负整数时,下标范围是 [0, count):包含零,不包含 count。
相比“从一开始、包含终点”的写法,这种形式直接对应 C 数组的合法下标。
6.3 步进方式可以变化
| 循环头 | 处理的值 |
|---|---|
for (int n = 5; n > 0; --n) |
5, 4, 3, 2, 1 |
for (int n = 0; n < 10; n += 2) |
0, 2, 4, 6, 8 |
for (int n = 1; n < 20; n *= 2) |
1, 2, 4, 8, 16 |
测试条件不必只比较计数次数,更新也不必只是加一。灵活性仍应服务于清楚的意图,避免把无关操作全部挤进循环头。
6.4 可以省略部分,但保留分号
for (; index < count;) {
++index;
}
省略初始化和更新后,可以在循环外或循环体中完成相应工作。
若省略中间的条件表达式,它按真处理,例如 for (;;) { ... }。这种循环通常需要通过 break、return 等方式结束。
循环体也可以修改步长或状态,但应确保仍然有正确的退出路径,不会意外使步长变成零或让整数溢出。
7. 复合赋值运算符
| 写法 | 对普通变量的基本效果 |
|---|---|
total += value |
total = total + value |
remaining -= used |
remaining = remaining - used |
amount *= factor |
amount = amount * factor |
value /= divisor |
value = value / divisor |
value %= divisor |
value = value % divisor |
它们与普通赋值一样具有较低优先级:
value *= step + 2;
对于普通变量,可按 value = value * (step + 2) 理解,而不是先乘 step 再加二。
更一般地说,复合赋值只求值左侧位置表达式一次。因此,当左侧含有下标更新等操作时,不能不加分析地把它机械展开为两个重复的左侧表达式。
复合赋值不会改变类型和范围规则。整数 /= 仍可能截断,%= 仍要求整数,有符号溢出也不会因为写法变短而消失。
8. 逗号运算符:有顺序的多个操作
8.1 常用于循环初始化和更新
int index;
int value;
for (index = 0, value = 10; index < 3; ++index, value += 5) {
printf("%d %d\n", index, value);
}
输出:
0 10
1 15
2 20
逗号运算符保证左边先求值,其副作用完成后再求值右边。整个逗号表达式的结果是右边表达式的结果。
int number = 2;
int result = (++number, number * 2);
结果是 number == 3、result == 6。这里的括号使整个逗号表达式参与初始化。
8.2 逗号也可能只是分隔符
| 写法 | 逗号的角色 |
|---|---|
index = 0, value = 10 |
在相应表达式语境中,是逗号运算符 |
int first, second; |
声明中的分隔符 |
printf("%d %d", first, second); |
实参分隔符 |
函数实参之间的逗号不保证按从左到右求值,不能把逗号运算符的顺序保证套用到它们上面。
逗号运算符的优先级低于赋值。value = 100, 200; 给 value 保存 100,而 value = (100, 200); 保存 200。这类写法容易误解,日常计算不应拿逗号当数字分组符号。
9. do while:先执行,再判断
int number = 5;
do {
printf("%d\n", number);
++number;
} while (number < 3);
虽然条件为假,这个循环仍输出一次 5。因为条件在循环体之后检查。
基本结构是:
do {
循环体
} while (条件);
结尾的分号是语法的一部分,不能省略。它与误写在普通 while 条件后面的空语句不同。
do while 适合“先做一次,再决定是否重试”的任务,例如先展示菜单或提示输入。但如果任务必须先获得许可才执行,就不能把许可判断放到最后,导致拒绝之后仍先执行一次。
10. 怎样选择和转换循环
| 形式 | 条件检查位置 | 最少执行次数 | 常见用途 |
|---|---|---|---|
while |
循环体之前 | 0 |
输入驱动、次数未知 |
for |
循环体之前 | 0 |
索引遍历、计数和明确步进 |
do while |
循环体之后 | 1 |
先执行一次的重试或交互 |
许多 for 可以重写为“初始化 + while + 更新”,但转换时不能只复制外形。第七章会介绍 continue:它在 for 中会先进入更新部分,而在 while 中直接回到条件,可能使两种代码行为不同。
选择循环时,还应确认:首次判断前数据是否有效,正常完成一轮是否推进,空输入和边界值是否正确结束。不要依靠有符号整数溢出使错误循环退出。
11. 嵌套循环
11.1 外层控制行,内层控制列
for (int row = 0; row < 3; ++row) {
for (int column = 0; column < 4; ++column) {
printf("*");
}
printf("\n");
}
输出:
****
****
****
每进入一次外层循环体,都会重新执行内层初始化 column = 0。内层完成四次之后,才输出本行换行,再进入下一行。
这里内层工作共执行 3 × 4 = 12 次,换行执行三次。循环范围固定且没有提前跳转时,可以通过次数相乘估计工作量。
11.2 内层边界可以依赖外层
for (int row = 1; row <= 3; ++row) {
for (int column = 0; column < row; ++column) {
printf("*");
}
printf("\n");
}
这次每行分别输出一、二、三个星号。总次数是 1 + 2 + 3,不能再用固定列数乘行数。
处理字母时,不应无条件假定所有实现都把字母连续编码;需要明确依赖 ASCII,或者通过字符串中的字符逐一访问。
12. 数组与循环
12.1 数组的合法下标
int scores[4] = {70, 81, 92, 83};
| 元素 | 值 |
|---|---|
scores[0] |
70 |
scores[1] |
81 |
scores[2] |
92 |
scores[3] |
83 |
数组包含四个相同类型、连续存放的元素。scores[4] 不是第五个可用位置,而是超出这个数组的有效元素范围。
数组元素可以像同类型变量一样参与计算和赋值。读取整型元素时,写 scanf("%d", &scores[index]);这与字符串输入时直接传数组首地址的用法要区分。
越界访问属于未定义行为,不能认为只会返回垃圾值或必然立即崩溃。编译器或检查工具可能发现部分问题,但语言不提供自动边界保护。
12.2 一个完整的统计程序
#include <stdio.h>
#define COUNT 4
int main(void)
{
int scores[COUNT] = {0};
int total = 0;
printf("Enter four integer scores from 0 to 100:\n");
for (int index = 0; index < COUNT; ++index) {
if (scanf("%d", &scores[index]) != 1) {
printf("Invalid input.\n");
return 1;
}
if (scores[index] < 0 || scores[index] > 100) {
printf("Score out of range.\n");
return 1;
}
}
printf("scores:");
for (int index = 0; index < COUNT; ++index) {
printf(" %d", scores[index]);
total += scores[index];
}
printf("\n");
double average = (double)total / COUNT;
printf("total: %d, average: %.2f\n", total, average);
return 0;
}
输入 70 81 92 83,输出结果中的两行为:
scores: 70 81 92 83
total: 326, average: 81.50
这个例子把读取和后续处理分开,同时检查每次读取是否成功。if 和逻辑或 || 的具体规则见第七章。
(double)total 在除法前取得浮点操作数,避免先发生整数除法。容量常量同时用于数组定义和循环边界,减少修改不一致的问题。
13. 用循环实现带返回值的函数
13.1 把计算结果返回调用方
#include <stdio.h>
double power_nonnegative(double base, int exponent);
int main(void)
{
double result = power_nonnegative(2.0, 10);
printf("result: %.0f\n", result);
printf("zero exponent: %.0f\n", power_nonnegative(2.0, 0));
return 0;
}
double power_nonnegative(double base, int exponent)
{
double result = 1.0;
for (int index = 0; index < exponent; ++index) {
result *= base;
}
return result;
}
输出:
result: 1024
zero exponent: 1
这个函数的使用前提是指数非负,并且计算结果符合所需范围与精度。它没有实现负指数,也不是数学库 pow 的完整替代品。
13.2 为什么初值是 1
乘法累计从 1 开始,经过零次乘法仍为 1,一次之后为 base,两次之后为平方。
如果初值为零,后面无论乘多少次都还是零。指数为零时,循环一次不执行,这也检验了初始化是否符合函数约定。
该算法把零次幂按空乘积返回 1;对于 0 的零次幂,也会得到 1,这是这个实现的约定,应与调用场景的数学需求区分。
13.3 原型、定义、调用、返回
| 环节 | 作用 |
|---|---|
| 函数原型 | 描述返回类型与参数类型,帮助检查调用 |
| 函数定义 | 实现计算步骤 |
| 函数调用 | 给出本次实参,开始执行函数 |
return result; |
结束函数,把结果值交给调用方 |
返回值可以保存到变量、参与其他计算,或者像第二条输出一样,直接作为另一个函数的实参。
return 与向屏幕打印是不同操作。函数可以只计算并返回,由调用方决定如何显示,这样更方便复用。
14. 有限次计算与数学极限
循环可以累加几何级数,例如前四项:
1 + 1/2 + 1/4 + 1/8 = 1.875
算法可以保存当前项 term,每轮执行 total += term,再令 term *= 0.5。
数学上,这个无穷级数趋近于 2;程序只能执行有限次运算,而且浮点精度有限。某次输出恰好显示为 2.000000,不代表程序真正完成了无穷多项求和。
实现重复计算前,可以先检查是否已有更简单的数学公式;实现之后,再区分有限项结果、显示舍入和理论极限。

