C程序中的循环语句
C程序中的循环语句
循环是重复执行其他语句的一种语句结构,它让程序能够自动化重复性任务。在C语言中,每个循环都有一个控制表达式,这个表达式就像一个守门员,决定着循环体是否继续执行。每次执行循环体时都要对控制表达式求值,如果表达式为真(即值不为零),循环继续;如果为假(值为零),循环终止。这种机制使得程序能够根据条件灵活地控制重复执行的次数。
while语句:循环的基石
while语句是C语言中最简单也是最基本的循环形式,它的语法结构清晰明了:while (表达式) 语句
。这里的圆括号是强制要求的,而且在右括号和循环体之间没有任何额外的关键字(不像某些语言需要do关键字)。执行while语句时,程序首先计算控制表达式的值,如果值不为零(真),就执行循环体,接着再次判定表达式,这个"判定-执行"的过程会持续进行直到控制表达式的值变为零。
让我们分析一个计算2的幂的例子,这个程序要找出大于或等于给定数n的最小的2的幂:
i = 1;
while (i < n)i = i * 2;
假设n的值为10,让我们逐步追踪这个循环的执行过程。初始时i为1,小于10,所以进入循环体,i变为2;第二次判定时2仍小于10,i变为4;第三次判定时4小于10,i变为8;第四次判定时8小于10,i变为16;第五次判定时16不小于10,循环终止。最终i的值是16,这正是大于或等于10的最小的2的幂。这个简单的例子展示了while循环的核心特性:控制表达式在循环体执行之前进行判定。
当循环体需要包含多条语句时,我们使用花括号将它们组合成复合语句。考虑这个倒计数程序:
i = 10;
while (i > 0) {printf("T minus %d and counting\n", i);i--;
}
这段代码从10开始倒计数,每次循环都输出一条消息并将i递减。程序首先检查i是否大于0(是的,10大于0),然后打印"T minus 10 and counting"并将i减为9。这个过程继续进行,依次打印9、8、7…直到打印"T minus 1 and counting"后i变为0。此时条件i > 0
不再成立,循环终止。
值得注意的是,while循环有一个重要特性:循环体可能一次都不执行。如果控制表达式在第一次判定时就为假,循环体会被完全跳过。比如在上面的倒计数程序中,如果i的初始值是0或负数,循环体将不会执行,程序不会打印任何消息。
while语句经常可以有多种写法。倒计数循环可以写得更加紧凑:
while (i > 0)printf("T minus %d and counting\n", i--);
这个版本将递减操作移到了printf函数调用内部,利用后缀递减运算符的特性:先使用i的值,然后再递减。这种写法虽然更简洁,但可能会牺牲一些可读性。
do语句:保证至少执行一次
do语句是while语句的变体,它们的关键区别在于控制表达式的判定时机。do语句的格式是:do 语句 while (表达式);
。注意末尾的分号是必需的。执行do语句时,先执行循环体,然后计算控制表达式的值。如果表达式的值非零,再次执行循环体,然后再次计算表达式。这种"执行-判定"的顺序保证了循环体至少执行一次。
用do语句重写倒计数程序:
i = 10;
do {printf("T minus %d and counting\n", i);--i;
} while (i > 0);
程序首先执行循环体,打印"T minus 10 and counting"并将i减为9。然后判定条件i > 0
,因为9大于0,所以继续执行循环。这个过程持续到打印"T minus 1 and counting"并将i减为0后,条件判定失败,循环终止。表面上看,这个程序与while版本的行为相同,但如果i的初始值是0或负数,do版本仍会打印一条消息,而while版本则不会。
do语句在需要至少执行一次循环体的场景中特别有用,一个经典的例子是计算整数的位数:
/* numdigit.c - 计算非负整数的位数 */
#include <stdio.h>int main(void)
{int digits = 0, n;printf("Enter a nonnegative integer: ");scanf("%d", &n);do {n /= 10; // 去掉最低位digits++; // 位数计数器加1} while (n > 0); // 如果还有数字,继续printf("The number has %d digit(s).\n", digits);return 0;
}
这个程序通过反复除以10来计算位数。关键在于,即使输入是0,它也有一位数字。如果使用while循环,当输入0时,循环体不会执行,程序会错误地报告"0位数字"。而do循环确保至少执行一次,正确地处理了这个边界情况。程序的工作原理是:每次除以10相当于去掉一位十进制数字,统计除法的次数就得到了位数。顺便提一下,无论需要与否,最好给所有的do语句都加上花括号。没有花括号的do语句很容易被误认为是while语句,造成阅读上的困惑。
for语句:最强大的循环结构
for语句是C语言中功能最强大、最灵活的循环结构,它的格式是:for (表达式1; 表达式2; 表达式3) 语句
。这三个表达式各有其职:表达式1是初始化步骤(只执行一次),表达式2控制循环的终止(每次循环前判定),表达式3是更新操作(每次循环后执行)。
让我们用for语句重写倒计数程序:
for (i = 10; i > 0; i--)printf("T minus %d and counting\n", i);
执行这个for语句时,首先i被初始化为10,然后判定i > 0
是否为真。因为10大于0,所以打印第一条消息,然后执行i--
将i减为9。接着再次判定i > 0
,循环继续。这个过程重复10次,i的值从10递减到1。
for语句和while语句关系紧密,大多数for循环都可以转换为等价的while循环:
表达式1;
while (表达式2) {语句表达式3;
}
应用这个模式,我们的倒计数for循环等价于:
i = 10;
while (i > 0) {printf("T minus %d and counting\n", i);i--;
}
这种转换帮助我们理解for语句的执行流程。表达式1是循环开始前的初始化,只执行一次;表达式2控制循环终止,只要它不为零,循环就继续;表达式3是每次循环的最后操作。由于第一个和第三个表达式都是以语句的方式执行的,它们的值并不重要,重要的是它们的副作用(如赋值或自增)。
for语句的惯用法
经过长期实践,C程序员总结出了一些for循环的标准写法。对于"向上加"或"向下减"的计数循环,常见的模式包括:
从0向上加到n-1(数组索引的典型用法):
for (i = 0; i < n; i++)/* 处理array[i] */
从1向上加到n(自然计数):
for (i = 1; i <= n; i++)/* 处理第i个元素 */
从n-1向下减到0(反向遍历数组):
for (i = n - 1; i >= 0; i--)/* 反向处理array[i] */
从n向下减到1(反向自然计数):
for (i = n; i > 0; i--)/* 反向处理第i个元素 */
遵循这些惯用法能帮助避免常见错误,如"差一错误"(off-by-one error)——这是循环边界条件设置不当导致的,可能多执行或少执行一次循环。
灵活运用for语句
for语句的强大之处在于它的灵活性。三个表达式可以任意省略,甚至可以全部省略。省略第一个表达式意味着没有初始化:
i = 10;
for (; i > 0; --i)printf("T minus %d and counting\n", i);
省略第三个表达式时,需要在循环体中更新控制变量:
for (i = 10; i > 0;)printf("T minus %d and counting\n", i--);
同时省略第一个和第三个表达式,for语句就变得像while语句:
for (; i > 0;)printf("T minus %d and counting\n", i--);
省略第二个表达式会创建无限循环(缺失的条件默认为真):
for (;;)/* 无限循环 */
这是创建无限循环的惯用写法,比while(1)
更受传统C程序员青睐。
C99引入了一个重要特性:可以在for语句中直接声明循环变量:
for (int i = 0; i < n; i++) {printf("%d ", i); // i在循环内可见
}
// printf("%d", i); // 错误!i在循环外不可见
这种写法将变量的作用域限制在循环内部,提高了代码的封装性和可读性。如果需要在循环外使用循环变量的最终值,则必须在循环外声明变量。
逗号运算符:在for语句中的妙用
逗号运算符允许将两个表达式"粘贴"成一个表达式。格式是表达式1, 表达式2
。执行时先计算表达式1(其值被丢弃),然后计算表达式2(作为整个逗号表达式的值)。逗号运算符是左结合的,优先级低于所有其他运算符。
在for循环中,逗号运算符常用于同时操作多个变量:
for (sum = 0, i = 1; i <= N; i++)sum += i;
这里在初始化部分同时设置了sum和i的值。逗号表达式sum = 0, i = 1
先将0赋给sum,然后将1赋给i。
让我们看一个更复杂的例子,展示for语句的极大灵活性:
/* square3.c - 使用奇数法打印平方表 */
#include <stdio.h>int main(void)
{int i, n, odd, square;printf("This program prints a table of squares.\n");printf("Enter number of entries in table: ");scanf("%d", &n);i = 1; // 要计算平方的数odd = 3; // 下一个要加的奇数for (square = 1; i <= n; odd += 2) {printf("%10d%10d\n", i, square);++i;square += odd;}return 0;
}
这个程序利用了数学性质:连续整数的平方之间的差是连续奇数(1, 3, 5, 7…)。for语句初始化square为1(1的平方),测试i是否小于等于n,每次循环后将odd增加2。循环体中,i递增,square加上当前的奇数得到下一个平方数。这种方法避免了乘法运算,在早期计算机上可能更高效。
循环控制语句:break、continue和goto
break语句:提前退出循环
break语句提供了从循环中间退出的能力。它将控制从包含它的最内层while、do、for或switch语句中转移出去。在寻找素数的例子中:
for (d = 2; d < n; d++)if (n % d == 0)break;if (d < n)printf("%d is divisible by %d\n", n, d);
elseprintf("%d is prime\n", n);
这个循环尝试用2到n-1之间的所有数去除n。一旦发现n能被某个数d整除(n % d == 0
),就没必要继续测试了,break语句立即退出循环。循环结束后,通过检查d的值可以判断循环是正常结束(d等于n,说明n是素数)还是提前退出(d小于n,说明找到了因子)。
break语句在读取用户输入直到特定值的场景中特别有用:
for (;;) {printf("Enter a number (enter 0 to stop): ");scanf("%d", &n);if (n == 0)break;printf("%d cubed is %d\n", n, n * n * n);
}
这是一个无限循环,只有当用户输入0时才通过break退出。注意break语句只能跳出一层嵌套,在嵌套结构中要特别注意。
continue语句:跳过本次迭代
continue语句不会退出循环,而是跳过当前迭代的剩余部分,直接进入下一次迭代。它将控制转移到循环体末尾之前的位置。看这个读取非零数并求和的例子:
n = 0;
sum = 0;
while (n < 10) {scanf("%d", &i);if (i == 0)continue; // 跳过0,继续读下一个数sum += i;n++;/* continue跳转到这里 */
}
程序读取整数,如果是0就跳过不计入总和,也不增加计数。只有非零数才会被加到sum中并使计数器n递增。这个循环会继续直到读取了10个非零数为止。
goto语句:无条件跳转
goto语句可以跳转到函数内任何有标号的语句处。标号是放在语句前的标识符,后面跟冒号。虽然goto在现代编程中很少使用,但在某些情况下它仍然有价值,特别是从深层嵌套中退出:
while (...) {switch (...) {...goto loop_done; /* break只能跳出switch,不能跳出while */...}
}
loop_done: ...
在这种情况下,break语句只能跳出switch,无法跳出外层的while循环。goto提供了一种直接跳出多层嵌套的方法。然而,过度使用goto会导致"意大利面条代码"(spaghetti code),使程序难以理解和维护。
空语句的艺术
空语句就是只有分号的语句。它在创建空循环体时特别有用。考虑素数判定的优化版本:
for (d = 2; d < n && n % d != 0; d++)/* 空循环体 */;
这里把除法测试移到了控制表达式中。循环继续的条件是d < n
且n % d != 0
(n不能被d整除)。一旦找到因子或d达到n,循环就终止。空语句通常单独放在一行,并加上注释,以明确这是有意为之。
程序员需要特别小心,避免无意中创建空语句。一个常见的错误是在控制结构后误加分号:
if (d == 0); /* 错误!创建了空语句 */printf("Error: Division by zero\n"); /* 总是执行 */while (i > 0); /* 错误!创建了无限循环 */
{printf("T minus %d and counting\n", i);--i;
}for (i = 10; i > 0; i--); /* 错误!循环体变成空语句 */printf("T minus %d and counting\n", i); /* 只执行一次,打印0 */
这些错误会导致程序行为完全不符合预期,调试时可能很难发现。
实践案例:完整的菜单驱动程序
让我们通过一个完整的账簿平衡程序来综合运用所学的循环知识:
/* checking.c - 账簿平衡程序 */
#include <stdio.h>int main(void)
{int cmd;float balance = 0.0f, credit, debit;printf("*** ACME checkbook-balancing program ***\n");printf("Commands: 0=clear, 1=credit, 2=debit, ");printf("3=balance, 4=exit\n\n");for (;;) { /* 无限循环,直到用户选择退出 */printf("Enter command: ");scanf("%d", &cmd);switch (cmd) {case 0: /* 清空账户 */balance = 0.0f;break;case 1: /* 存款 */printf("Enter amount of credit: ");scanf("%f", &credit);balance += credit;break;case 2: /* 取款 */printf("Enter amount of debit: ");scanf("%f", &debit);balance -= debit;break;case 3: /* 显示余额 */printf("Current balance: $%.2f\n", balance);break;case 4: /* 退出程序 */return 0; /* 直接返回,结束程序 */default: /* 无效命令 */printf("Commands: 0=clear, 1=credit, 2=debit, ");printf("3=balance, 4=exit\n\n");break;}}
}
这个程序展示了几个重要概念的综合应用。首先,使用for(;;)
创建了程序的主循环,这是一个无限循环,只有通过return语句才能退出。其次,switch语句处理用户的菜单选择,每个case对应一个功能。注意case 4中使用return 0直接结束整个程序,这比使用break加标志变量更简洁。程序维护一个浮点数balance来跟踪账户余额,通过简单的加减操作模拟存取款。
另一个值得研究的程序是改进版的平方表生成器:
/* square2.c - 使用for语句打印平方表 */
#include <stdio.h>int main(void)
{int i, n;printf("This program prints a table of squares.\n");printf("Enter number of entries in table: ");scanf("%d", &n);for (i = 1; i <= n; i++)printf("%10d%10d\n", i, i * i);return 0;
}
这个程序使用%10d
格式说明符使输出对齐成整齐的列。数字在10个字符宽度内右对齐,创建了美观的表格输出。
附录:代码解析
A. 循环优化技巧
将普通循环转换为空循环体形式有时能提高代码的简洁性。考虑这个数列求和程序的演变:
/* sum.c - 数列求和(原始版本) */
#include <stdio.h>int main(void)
{int n, sum = 0;printf("This program sums a series of integers.\n");printf("Enter integers (0 to terminate): ");scanf("%d", &n);while (n != 0) {sum += n;scanf("%d", &n);}printf("The sum is: %d\n", sum);return 0;
}
这个程序有两个相同的scanf调用,一个在循环前,一个在循环内。这种重复在使用while循环时很常见。程序的逻辑是:先读一个数,如果不是0就加到总和中,然后读下一个数。条件n != 0
在数被读入后立即判定,确保0不会被加到总和中。
B. 循环变体分析
前面展示的利用奇数计算平方的程序体现了一个数学原理:n² = 1 + 3 + 5 + … + (2n-1)。让我们追踪程序执行来验证这一点:
- i=1: square=1(正确,1²=1)
- i=2: square=1+3=4(正确,2²=4)
- i=3: square=4+5=9(正确,3²=9)
- i=4: square=9+7=16(正确,4²=16)
这个算法在没有硬件乘法器的早期计算机上特别有价值。现代编译器的优化使得这种技巧的性能优势不再明显,但它仍然是理解for语句灵活性的绝佳例子。
C. 嵌套循环的goto应用
虽然goto通常应该避免,但在某些情况下它是最清晰的解决方案。考虑在二维数组中搜索特定值:
for (i = 0; i < m; i++)for (j = 0; j < n; j++)if (a[i][j] == target)goto found;
/* 没找到目标 */
printf("Target not found\n");
goto done;found:printf("Found at position (%d, %d)\n", i, j);
done:/* 继续程序 */
使用break只能跳出内层循环,需要额外的标志变量才能跳出外层循环。goto在这里提供了直接而清晰的解决方案。
D. while与for的微妙差异
虽然大多数for循环可以转换为while循环,但含有continue语句时情况会变复杂:
/* for循环版本 */
sum = 0;
for (n = 0; n < 10; n++) {scanf("%d", &i);if (i == 0)continue;sum += i;
}/* 错误的while转换 */
sum = 0;
n = 0;
while (n < 10) {scanf("%d", &i);if (i == 0)continue;sum += i;n++; /* continue会跳过这里! */
}
在for循环中,continue跳转到更新表达式(n++)之前,所以n总会递增。但在while循环中,continue跳过了n++,导致循环可能永不终止。正确的while版本需要不同的结构。
E. 循环不变式的概念
理解循环的一个强大工具是循环不变式——在每次迭代开始时都为真的条件。例如,在计算2的幂的循环中:
i = 1;
while (i < n)i = i * 2;
/* 循环不变式:i是2的幂 */
循环不变式是:i始终是2的某个幂。初始时i=1=2⁰,每次迭代将i乘以2保持这个性质。循环终止时,不变式仍然成立,且i >= n
,因此i是大于等于n的最小2的幂。
F. 防御性编程实践
在实际编程中,应该考虑边界情况和错误处理。例如,改进的数字位数计算程序:
#include <stdio.h>
#include <limits.h>int main(void)
{int digits = 0, n;printf("Enter a nonnegative integer: ");if (scanf("%d", &n) != 1) {printf("Invalid input\n");return 1;}if (n < 0) {printf("Number must be nonnegative\n");return 1;}/* 处理特殊情况 */if (n == 0) {printf("The number has 1 digit(s).\n");return 0;}/* 一般情况 */while (n > 0) {n /= 10;digits++;/* 防止溢出 */if (digits > 10) { /* int最多10位 */printf("Number too large\n");return 1;}}printf("The number has %d digit(s).\n", digits);return 0;
}
这个版本增加了输入验证、负数检查和溢出保护,使程序更加健壮。
G. 性能考虑
虽然现代编译器优化很强大,但了解循环的性能特性仍然重要。例如,向下计数循环通常比向上计数略快:
/* 向上计数 */
for (i = 0; i < n; i++)process(i);/* 向下计数(可能更快) */
for (i = n - 1; i >= 0; i--)process(i);
向下计数的优势在于比较操作(与0比较)可能比与变量比较更快。但这种差异在现代处理器上通常可以忽略不计,代码的清晰性应该优先于微小的性能差异。