清风日常

Stay curious. Stay gentle.

听一会儿

4 首收藏
LOVE 2000 封面

LOVE 2000

遠野ひかる

0:004:23
首页/C Primer Plus/正文

C Primer Plus(第6版)· 第6章:C 控制语句:循环

循环把重复任务写成一套规则。设计时需要明确:从什么状态开始,在什么条件下继续,每轮怎样推进,结束后留下什么结果。

文章目录 39 个章节

第6章:C 控制语句:循环

循环把重复任务写成一套规则。设计时需要明确:从什么状态开始,在什么条件下继续,每轮怎样推进,结束后留下什么结果。

本章把 while、for 和 do while 放在一起理解,并进一步用循环处理数组和实现带返回值的函数。

1. 先区分两种重复任务

任务类型 特点 例子
计数循环 次数或索引范围明确 依次处理数组中的四个元素
不确定次数循环 次数由输入或运行状态决定 不断读数,直到读取失败

“不确定次数”不等于没有结束条件,只是执行前不知道到底会重复多少次。

如果已知初始化、边界和步进,for 往往更容易表达;如果主要依据输入成功与否等条件推进,while 往往更自然。

2. while:先判断,再执行

2.1 基本结构

c
int count = 0;

while (count < 3) {
    printf("%d\n", count);
    ++count;
}

输出 0、1、2,退出后 count 为 3。

text
初始化
  ↓
判断条件 ←────────┐
  ├─ 假 → 离开    │
  └─ 真 → 循环体  │
           ↓      │
        更新状态 ─┘

while 控制紧随其后的一条语句。要重复多个步骤,应使用花括号构成复合语句。

2.2 条件只在执行到判断处时检查

c
int count = 0;

while (count < 2) {
    ++count;
    printf("count: %d\n", count);
}

第二轮中,count 增加到 2 后,仍会执行本轮后面的输出。程序回到条件处,发现 count < 2 为假,才结束循环。

循环不会因为某个变量刚发生变化,就自动中断当前循环体。

2.3 空语句与花括号

c
while (count < 3);

这里的循环体是一个空语句。若条件不会变化,程序可能一直停在这个循环中。

有时空循环体是有意设计的,例如工作已全部在条件表达式里完成。此时应把空语句单独放一行并说明用途,避免与误写分号混淆。

循环体的范围由语法决定,缩进只帮助阅读。遗漏花括号,可能让负责更新变量的语句实际位于循环外。

3. 通过输入状态控制循环

3.1 数据与读取状态是不同的值

c
long number = 0;
int status = scanf("%ld", &number);
  • number 接收用户输入的数值。
  • status 接收成功赋值的项目数,或输入失败状态。

用户输入 0 时,如果读取成功,status 仍是 1。不能把数据为零与读取失败混为一谈。

3.2 把读取与判断合并

c
#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。零为假,非零为真,因此负数也为真。

c
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 优先级与括号

算术运算通常先于关系比较,关系比较又先于赋值:

text
x > y + 2     → x > (y + 2)
flag = x > y  → flag = (x > y)

< <= > >= 的优先级高于 == !=。连续比较并不会自动变成数学区间判断,应分别写出完整条件,再用逻辑运算符组合;第七章会详细展开。

字符可以按数值编码比较,但 name1 == name2 不能用来判断两段字符串的内容是否相同。

4.4 用布尔变量表达状态

c
#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 先决定比较的是“完全相同”还是“足够接近”

浮点计算可能发生舍入。若算法依赖多次累加小数后恰好等于目标值,循环可能不按预期结束。

c
for (int step = 0; step < 10; ++step) {
    double value = step / 10.0;
    printf("%.1f\n", value);
}

如果需要十个采样点,用整数控制次数,再计算对应浮点值,比依赖浮点变量精确到达某个终点更容易检查。

这不意味着浮点数永远不能使用 ==。是否要求精确相等,应由数据来源和算法语义决定。

5.2 容差比较应与问题尺度相配

对于限定范围内的有限数值,若任务允许绝对误差为 0.001,可以写:

c
fabs(actual - expected) <= 0.001

fabs 声明在 <math.h> 中,返回浮点绝对值。这个判断表示“差值不超过允许范围”。

容差需要根据单位、数量级和任务要求选择,不能把一个固定小数或 DBL_EPSILON 当成所有问题通用的阈值。涉及无穷大、NaN 等特殊值时,还要另行判断。

6. for:集中表达初始化、判断和更新

6.1 执行顺序

c
for (int index = 0; index < 4; ++index) {
    printf("%d\n", index);
}

输出 0、1、2、3。

text
初始化,只执行一次
       ↓
判断条件 ←────────────┐
  ├─ 假 → 离开       │
  └─ 真 → 循环体     │
            ↓        │
         更新表达式 ─┘

正常完成一轮循环体后,先执行更新,再进行下一次判断。第一次判断之前不执行更新。

若没有提前跳出,循环体执行四次,条件通常判断五次。最后一次判断用于发现循环应结束。

C99/C11 允许在初始化部分声明变量。这里的 index 作用域包括 for 语句,循环外不能继续使用这个局部名称。

6.2 半开区间更适合数组下标

c
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 可以省略部分,但保留分号

c
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

它们与普通赋值一样具有较低优先级:

c
value *= step + 2;

对于普通变量,可按 value = value * (step + 2) 理解,而不是先乘 step 再加二。

更一般地说,复合赋值只求值左侧位置表达式一次。因此,当左侧含有下标更新等操作时,不能不加分析地把它机械展开为两个重复的左侧表达式。

复合赋值不会改变类型和范围规则。整数 /= 仍可能截断,%= 仍要求整数,有符号溢出也不会因为写法变短而消失。

8. 逗号运算符:有顺序的多个操作

8.1 常用于循环初始化和更新

c
int index;
int value;

for (index = 0, value = 10; index < 3; ++index, value += 5) {
    printf("%d %d\n", index, value);
}

输出:

text
0 10
1 15
2 20

逗号运算符保证左边先求值,其副作用完成后再求值右边。整个逗号表达式的结果是右边表达式的结果。

c
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:先执行,再判断

c
int number = 5;

do {
    printf("%d\n", number);
    ++number;
} while (number < 3);

虽然条件为假,这个循环仍输出一次 5。因为条件在循环体之后检查。

基本结构是:

text
do {
    循环体
} while (条件);

结尾的分号是语法的一部分,不能省略。它与误写在普通 while 条件后面的空语句不同。

do while 适合“先做一次,再决定是否重试”的任务,例如先展示菜单或提示输入。但如果任务必须先获得许可才执行,就不能把许可判断放到最后,导致拒绝之后仍先执行一次。

10. 怎样选择和转换循环

形式 条件检查位置 最少执行次数 常见用途
while 循环体之前 0 输入驱动、次数未知
for 循环体之前 0 索引遍历、计数和明确步进
do while 循环体之后 1 先执行一次的重试或交互

许多 for 可以重写为“初始化 + while + 更新”,但转换时不能只复制外形。第七章会介绍 continue:它在 for 中会先进入更新部分,而在 while 中直接回到条件,可能使两种代码行为不同。

选择循环时,还应确认:首次判断前数据是否有效,正常完成一轮是否推进,空输入和边界值是否正确结束。不要依靠有符号整数溢出使错误循环退出。

11. 嵌套循环

11.1 外层控制行,内层控制列

c
for (int row = 0; row < 3; ++row) {
    for (int column = 0; column < 4; ++column) {
        printf("*");
    }
    printf("\n");
}

输出:

text
****
****
****

每进入一次外层循环体,都会重新执行内层初始化 column = 0。内层完成四次之后,才输出本行换行,再进入下一行。

这里内层工作共执行 3 × 4 = 12 次,换行执行三次。循环范围固定且没有提前跳转时,可以通过次数相乘估计工作量。

11.2 内层边界可以依赖外层

c
for (int row = 1; row <= 3; ++row) {
    for (int column = 0; column < row; ++column) {
        printf("*");
    }
    printf("\n");
}

这次每行分别输出一、二、三个星号。总次数是 1 + 2 + 3,不能再用固定列数乘行数。

处理字母时,不应无条件假定所有实现都把字母连续编码;需要明确依赖 ASCII,或者通过字符串中的字符逐一访问。

12. 数组与循环

12.1 数组的合法下标

c
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 一个完整的统计程序

c
#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,输出结果中的两行为:

text
scores: 70 81 92 83
total: 326, average: 81.50

这个例子把读取和后续处理分开,同时检查每次读取是否成功。if 和逻辑或 || 的具体规则见第七章。

(double)total 在除法前取得浮点操作数,避免先发生整数除法。容量常量同时用于数组定义和循环边界,减少修改不一致的问题。

13. 用循环实现带返回值的函数

13.1 把计算结果返回调用方

c
#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;
}

输出:

text
result: 1024
zero exponent: 1

这个函数的使用前提是指数非负,并且计算结果符合所需范围与精度。它没有实现负指数,也不是数学库 pow 的完整替代品。

13.2 为什么初值是 1

乘法累计从 1 开始,经过零次乘法仍为 1,一次之后为 base,两次之后为平方。

如果初值为零,后面无论乘多少次都还是零。指数为零时,循环一次不执行,这也检验了初始化是否符合函数约定。

该算法把零次幂按空乘积返回 1;对于 0 的零次幂,也会得到 1,这是这个实现的约定,应与调用场景的数学需求区分。

13.3 原型、定义、调用、返回

环节 作用
函数原型 描述返回类型与参数类型,帮助检查调用
函数定义 实现计算步骤
函数调用 给出本次实参,开始执行函数
return result; 结束函数,把结果值交给调用方

返回值可以保存到变量、参与其他计算,或者像第二条输出一样,直接作为另一个函数的实参。

return 与向屏幕打印是不同操作。函数可以只计算并返回,由调用方决定如何显示,这样更方便复用。

14. 有限次计算与数学极限

循环可以累加几何级数,例如前四项:

text
1 + 1/2 + 1/4 + 1/8 = 1.875

算法可以保存当前项 term,每轮执行 total += term,再令 term *= 0.5。

数学上,这个无穷级数趋近于 2;程序只能执行有限次运算,而且浮点精度有限。某次输出恰好显示为 2.000000,不代表程序真正完成了无穷多项求和。

实现重复计算前,可以先检查是否已有更简单的数学公式;实现之后,再区分有限项结果、显示舍入和理论极限。

Aron 的清风日常

作者 Aron · 发布于 2026.07.22

愿这份记录能为你提供一点参考。发现问题,欢迎通过 GitHub 或 邮件交流。

如果这篇记录对你有用,留下一份喜欢。

← 所有文章继续阅读C Primer Plus(第6版)· 附录整理:语法与标准库速查 →
SEARCH MY NOTES

在记录里,找一点答案

搜索标题、标签与全文 · 按 Esc 关闭