如第1章所述,while 和 for 在循环顶部检查结束条件。与之相反,C语言的第三个循环,do-while 是每轮循环的主体走完之后,在底部检查结束条件;循环体至少会执行一次。
do 的语法为
do
语句
while (表达式);
先执行语句,然后对表达式求值。如果为真,则继续执行语句,接着再对表达式求值,就这样循环往复。当表达式变为假时,循环结束。除了检查的含义外,do-while 等价于Pascal的 repeat-until语句【Pascal的 until 判断为真则循环退出,与C语言的 while 表达式判断为假才退出,含义正好相反】。
经验显示 do-while 用得比 while 和 for 少得多。然而,有时它也是有价值的,比如下面这个函数 itoa,将一个数转换成字符串( atoi 的反向处理)。这活儿比你一开始可能想的要稍微复杂些,因为用简单方法生成的字符串顺序是错误的。我们选择反向生成字符串,然后将其翻转。【reverse函数见上一节】
/* itoa: 把n转换成字符串,存入s */
void itoa(int n, char s[])
{
int i, sign;
if ((sign = n) < 0) /* 记录符号 */
n = -n; /* 使n为正数 */
do { /* 倒序生成字符串 */
s[i++] = n % 10 + '0'; /* 获取下一个数位 */
} while ((n /= 10) > 0) /* 删除该数位 */
if (sign < 0)
s[i++] = '-';
s[i] = '\0';
reverse(s);
}
这里用 do-while 是必要的,至少是方便的,因为至少要有一个字符被放到数值 s 中,即使 n 是 0。尽管大括号不是必需的,我们还是把构成 do-while 主体的单条语句包在其中,避免草率的读者把 while 部分错看成是一个 while 循环的开始。
练习3-4、在数字用2的补码表示的机器上,我们的 itoa 版本无法处理最大的负数,即当 n = -(2的字长 -1 次方)。解释为何如此。修改程序使之打印正确的值,不管它跑在什么机器上。
练习3-5、写函数 itob(n ,s, b) 将整数 n 转换成以 b 为基数的字符串并存入字符串 s 中。特别说明,itob(n, s, 16) 把 n 格式化为 16进制数存入s中。
练习3-6、写个一版接受3个而不是2个参数的 itoa 函数。第三个参数为最小域宽度;有必要的话,在转换后的数字左侧填充空格使其达到足够的宽度。
不通过顶部或底部的条件检查而直接从循环中退出,有时会比较方便。break 语句提供了从 for、while 和 do 中提前退出的方法,还包括前面说过的 switch 。break 会使它所在的最内层循环或 switch 马上退出。
下面的函数 trim 从字符串末尾删除结尾的空格,制表符和换行符,当发现最右边的非空格、非制表符、非换行符时,它使用 break 从循环中退出。
/* trim: 删除末尾的空格、指标和换行符 */
int trim(char s[])
{
int n;
for (n = strlen(s)-1; n >= 0; n--)
if (s[n] != ' ' && s[n] != '\t' && s[n] != '\n')
break;
s[n+1] = '\0';
return n;
}
strlen 返回字符串的长度。for 循环从末尾开始反向扫描,寻找第一个非空格、非制表、非换行符。当找到一个这样的字符,或者 n 变为负数时(此时整个字符串都扫描完了),循环结束。你应该验证,即使在字符串为空或者只包含空白字符的时候,这个处理也是正确的。
continue 语句与 break 相关,但用的少一些;它会使其所在的 for、while 和 do 循环开始下一轮迭代。在 while 和 do 中,这意味着马上执行括号内的检查部分;而对于 for, 控制转移到递增的步骤【即表达式3】。continue 语句只用于循环,不用于 switch。若循环内有 switch 且 switch 内有 continue 时,这个 continue 会使循环进入下个迭代。
例如,下面这个代码段只处理数组 a 中的非负元素;负值都被跳过了
for (i = 0; i < n; i++) {
if (a[i] < 0) /* 跳过负元素 */
continue;
... /* 处理正元素 */
}
continue 语句经常用在这种情况:当循环后面的一部分非常复杂,而把测试条件反转然后再加一层缩进,会让程序嵌套太深【而影响阅读/维护】。
C提供了可被无限滥用的 goto 语句,以及跳转的标号。正式地说,goto 并非必要,而且实践中总是很容易写出不用 goto 的代码。本书中我们还没用过 goto。
然而,还是存在一些也许可以使用 goto 的场景。最常用的是从某些深深嵌套的结构中放弃处理,比如从两层或更多层的循环中马上跳出来。不能直接使用 break 语句是因为它只能退出最内层的循环。像这样:
for (...)
for (...) {
if (disaster)
goto error;
}
...
error:
善后处理
如果错误处理代码很重要,而且错误会发生在多处,则这个代码组织方式是很方便的。
标号的形式与变量名相同,后面跟着冒号。标号可以加到与 goto 所在同一函数的任一语句前面。标号的作用域是整个函数。
另一个例子是判断两个数组 a 和 b 是否有相同的一个元素。一种可能的写法是:
for (i = 0; i < n; n++)
for (j = 0; j < m; j++)
if (a[n] == b[m])
goto found;
/* 没找到相同元素的处理 */
...
found:
/* 找到 a[i] == b[j] */
...
涉及 goto 的代码总是能写成不带 goto的,尽管可能的代价是一些重复的检查或一个额外的变量。例如,上面的例子改写为
found = 0;
for (i = 0; i < n && !found; i++)
for (j = 0; j < m && !found; j++)
if (a[n] == b[m])
found = 1;
if (found)
/* 找到 a[i-1] == b[j-1] */
...
else
/* 没找到相同的元素 */
...
除了这里举出的几个例外,依赖于 goto 语句的代码通常总是比没有 goto 的代码更难理解、更难维护。尽管我们不想在这个问题上说的太武断,但确实看起来 goto 应该少用,如果不是说完全不用的话。
(第三章完)