3.4 switch
switch语句是一个多路决策,用来检查一个表达式是否与若干整数常量值之一匹配,并进入对应的分支。
switch (expression) {
case const-expr: statements
case const-expr: statdments
default: statments
}
每个 case 都以一个或多个整数值的常量或常量表达式为标号,如果 switch 后面的表达式的值与某个 case 相匹配,则代码从该 case 开始执行。所有 case 的表达式都必须不同。如果所有 case 都不满足,则执行 default 对应的语句。default 是可选的;也就是说,如果没有 default 且与所有case 都不匹配,则不会发生任何动作。各个 case 以及 default 之间的顺序可以随意。
在第一章,我们写了个程序来计算每个数位,空格和其他字符的数量,使用的是 if ... else if ... else 的序列。下面这个程序使用 switch 达到相同的效果。
#include <stdio.h>
main() /* 计算数字,空格和其他字符的数量 */
{
int c, i, nwhite, nother, ndigit[10];
nwhite = nother = 0;
for (i = 0; i < 10; ++i)
ndigit[i] = 0;
while ((c = getchar()) != EOF) {
switch (c) {
case '0': case '1': case '2': case '3': case '4':
case '5': case '6': case '7': case '8': case '9':
ndigit[c-'0']++;
break;
case ' ':
case '\t':
case '\n':
nwhite++;
break;
default:
nother++;
break;
}
}
printf("digits =");
for (i = 0; i < 10; ++i)
printf(" %d", ndigit[i]);
printf(", white space = %d, other = %d\n",
nwhite, nother);
return 0;
}
break 语句使代码执行从 switch 中直接退出。因为 case 其实就是用作(汇编语言的)标号,因此当这个 case 的代码执行完毕之后,会继续执行(fall through)到下面的 case ,除非你显式地跳出。 break 和 return 是退出 switch 的最常用方式。break 语句还能用于强制从 while、for 和 do循环内部立即退出,不过这些会在本章后面讨论。
case 的继续执行有利也有弊。在利的方面,它允许多个 case 对应同一个动作,正如本例中多个数位对同一数组操作。另外,这也必然要求:正常情况下所有分支都必须以 break 结尾,以防止继续执行到下面的分支。从一个 case 分支继续往下执行的这种代码是不健壮的,当程序修改时很容易解体【即case之间的这种关联很容易被不小心拆散,比如调整case顺序、插入带break的case等等】。除了多个标号对应单个计算的场景之外,其他情况下都应该谨慎使用 继续执行,并加上注释。
尽管逻辑上没必要,但在最后一个分支后面加上 break(本例中是 default),是不错的代码风格。某天,当其他 case 被加到末尾时,这一丁点的防御性编程措施会拯救你。
练习3-2、写个函数 escape(s, t) ,当它将字符串 s 拷贝到 t 中时,把换行符和制表符这类字符转换成可见的转义序列如 \t \n。要使用 switch。再写个函数做反方向的转换,把转义序列转成实际的字符。
3.5 while 和 for 循环
我们已经遇到过 while 和 for 循环。在
while (表达式)
语句
中,对表达式求值,如果值非0,则执行语句,并再次对表达式求值。这个循环一直继续下去,直到表达式变为0,此时会继续执行 while 循环结构之后的代码。
而 for 语句
for (表达式1; 表达式2; 表达式3)
语句
等价于
表达式1
while (表达式2) {
语句
表达式3 ;
}
唯一区别是在有 continue 的情况下,这个会在3.7节说明。
从语法上说,for 循环的三个组成部分都是表达式。最常见的情况下,表达式1和表达式3是赋值或函数调用,表达式2是关系表达式。三部分都可以各自省略,不过分号必须保留。如果去掉了表达式1和表达式3,那仅仅是把相对于 while 的扩展部分去掉了【退化成了while】。如果没有表达式2,则认为永远为真,因此
for (;;) {
...
}
是一个“无限”循环,可能通过其他方法来退出,比如 break 或 return。
用 while 还是 for,很大程度上是个人偏好的问题。比如
while ((c = getchar()) == ' ' || c == '\n' || c == '\t')
; /* 跳过空白字符 */
这里没有初始化或重初始化,因此用 while 是最自然的。
当有一个简单的初始化和自增时,for 是更合适的,因为它将循环控制语句紧密结合在一起,放在循环的头部【非常利于代码阅读/修改】。最明显的如
for (i = 0; i < n; ++i)
...
这是C语言处理数组前 n 个元素的惯用写法,可类比于 Fortran 的 DO 循环或 Pascal 的 for 。然而这个类比不够完美,因为 C 的 for 循环可以在循环内修改索引和上限,而且索引变量 i 会保持循环结束时的值,不管循环由于什么原因结束。 由于 for 循环的各个部分可以是任意表达式,因此 for 循环并不限于使用等差数列。然而,把不相干的多个计算分别硬塞到 for 循环的初始化和递增部分中,会是糟糕的代码风格,这些位置最好保留给循环控制操作来使用。
下面这个稍大的例子,是另一个版本的 atoi,它将字符串转换为对应的数字。这个版本比第二章的稍微通用些;它处理了可能存在的前导空格,以及 + 号和 - 号。(第四章会展示 atof,它将字符串转换成浮点数)
程序的结构反映了输入的格式:
如果有空格,则跳过
如果有符号,则获取符号
获取整数部分,并进行转换
每一步都做自己的事,并把干净的状态留给后面的部分。当遇到首个不能正确构成数字的字符时,整个过程结束。
#include <ctype.h>
/* atoi: 把s转换为整数,第二版 */
int atoi(char s[])
{
int i, n, sign;
for (i = 0; isspace(s[i]); i++) /* 跳过空格 */
;
sign = (s[i] == '-' ) ? -1 : 1;
if (s[i] == '-' || s[i] == '+') /* 跳过符号 */
i++;
for (n = 0; isdigit(s[i]); i++)
n = 10 * n + (s[i] - '0');
return sign * n;
}
标准库提供了一个更复杂的函数 strtol 将字符串转换为长整数,见附录B的第5节。
当存在多个嵌套的循环时,把循环控制集中在一起的好处更加明显。下面的函数是对整型数组的Shell 排序。这个1959年由 D.L.Shell 发明的排序算法,其基本思想是在早期对相隔较远的元素进行比较,而不像那些简单的交换排序算法是对相邻的元素进行比较。这样往往能快速消灭大量的无序,后期的工作就少一些。比较元素的间隔逐渐减为1,此时的排序实际就成为了相邻交换。
/* shellsort: 升序排列 v[0]...v[n-1] */
void shellsort(int v[], int n)
{
int gap, i, j, temp;
for (gap = n/2; gap > 0; gap /= 2)
for (i = gap; i < n; i++)
for (j = i-gap; j>=0 && v[j]>v[j+gap]; j-=gap) {
temp = v[j];
v[j] = v[j+gap];
v[j+gap] = temp;
}
}
这里有三层循环。最外层循环控制所比较元素的间隔,从 n/2 开始,每轮除以2,直到为0。中间的循环按逐个元素步进。最内层循环比较每对间隔为 gap 的元素,并翻转无序的元素对。由于 gap 最终减为1,所有的元素也最终被正确排序。注意最外层的循环,尽管它不是等差数列,但 for 的通用性仍然使它与其他的循环保持相同的形式。
现在介绍最后一个C语言操作符,逗号“,”,它最常使用在 for 语句中。由逗号分隔的一对表达式,其求值顺序为从左到右,而结果的类型和值,分别为右侧操作数的类型和值。这样的话,就可能将多个表达式放到 for 语句中不同的部分,例如同时处理两个索引。下面函数 reverse 展示了这种用法,该函数在原地翻转字符串 s 。
#include <sring.h>
/* reverse: 原地翻转字符串s */
void reverse(char s[])
{
int c, i, j;
for (i = 0, j = strlen(s)-1; i < j; i++, j--) {
c = s[i];
s[i] = s[j];
s[j] = c;
}
}
用于分隔函数参数的逗号,还有变量声明中的逗号,等等这些都不是逗号操作符,并且不保证从左到右求值。
逗号操作符应当谨慎使用。最适合之处是存在强关联的多个结构中,例如在 reverse 的 for 循环之中,另外是在多步操作必须为单一表达式的宏里面。逗号表达式也许还适用于交换 reverse 中的元素,其中的交换可认为是一个操作:
for (i = 0, j = strlen(s)-1; i < j; i++, j--)
c = s[i], s[i] = s[j], s[j] = c;
练习3-3、写个函数 expand(s1, s2) ,将s1的简化表示法在s2中展开为其完整形式,如 a-z 展开为 abcdefg.......uvwxyz。支持大小写字母和数字,能够处理 a-b-c 和 a-z0-9 以及-a-z。前导或末尾的 - 号当普通文本处理。