C语言K&R圣经笔记 3.4 switch 3.5 while和for循环

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。前导或末尾的 - 号当普通文本处理。

  • 6
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值