《C程序设计语言》笔记 第3章 控制流
3.1 语句与程序块
在表达式之后加上一个分号就变成了语句(statement)(表达式语句)。例如:
1
2
3
x = 0;
i++;
printf("Hello, world\n");
在C语言中,分号是语句结束符。
用一对花括号把一组声明和语句括在一起就构成了一个复合语句(compound statement),也叫作程序块(block)。复合语句在语法上等价于单条语句。 例如函数体,以及if
、else
、while
和for
之后的语句。右花括号用于结束程序块,其后不需要分号。
3.2 if-else语句
if-else
语句用于条件判定。其语法如下:
1
2
3
4
if (表达式)
语句1
else
语句2
其中else
部分是可选的。该语句执行时,先计算表达式的值,如果其值为真(即表达式的值为非0),则执行语句1;如果其值为假(即表达式的值为0),并且该语句包含else
部分,则执行语句2。其中语句既可以是单条语句,也可以是括在花括号内的复合语句。
由于if
语句只是简单地测试表达式的数值,因此if (expr != 0)
等价于if (expr)
;if (expr == 0)
等价于if (!expr)
。
因为if-else
语句的else
部分是可选的,所以在嵌套的if
语句中省略else
部分将导致歧义。解决的方法是将每个else
与最近的一个没有else
配对的if
进行匹配(就近原则)。例如,在下列语句中:
1
2
3
4
5
if (n > 0)
if (a > b)
z = a;
else
z = b;
else
部分与内层的if
匹配,通过缩进也可以看出来(缩进并不能决定匹配关系,即使else
不缩进也会与内层if
匹配,因为编译器会忽略所有的空白符)。如果想要与外层if
匹配,则必须使用花括号(没有实际意义,只是为了说明):
1
2
3
4
5
6
if (n > 0) {
if (a > b)
z = a;
}
else
z = b;
这种错误很难发现,因此建议在有嵌套的if
语句时使用花括号。
3.3 else-if语句
在C语言中经常会用到下列结构:
1
2
3
4
5
6
7
8
9
if (表达式1)
语句1
else if (表达式2)
语句2
else if (表达式3)
语句3
...
else
语句n
这种if
语句序列是编写多路判定最常用的方法。其中的各表达式将被依次求值,一旦某个表达式结果为真,则执行对应的语句,并终止整个语句序列的执行。其中各语句既可以是单条语句,也可以是括在花括号内的复合语句。最后一个else
部分用于处理“上述条件均不成立”的情况或默认情况,可省略。
这里通过一个二分查找(折半查找)函数binsearch
说明三路判定程序的用法。该函数用于判定已排序的数组v中是否存在某个特定的值x。如果v中包含x,则该函数返回x在v中的位置(0~n-1);否则返回-1。
在二分查找时,首先将x与数组v的中间元素进行比较。如果x小于中间元素,则在数组的前半部分查找;否则在数组的后半部分查找。这个过程一直进行下去,直到找到指定的值或查找范围为空。
例如,下图是在数组{2, 10, 18, 22, 24, 30, 35}中查找24的过程:
练习3-1 上面的折半查找在循环内执行了两次测试,其实只要一次就足够(代价是将更多的测试在循环外执行)。重写该函数,使得在循环内部只执行一次测试。比较两种版本函数的运行时间。
执行次数(万) | binsearch运行时间(ms) | binsearch2运行时间(ms) |
---|---|---|
100 | 101 | 94 |
200 | 205 | 194 |
500 | 509 | 484 |
1000 | 991 | 954 |
2000 | 2058 | 1971 |
3000 | 3061 | 2965 |
4000 | 4089 | 3924 |
5000 | 5161 | 4897 |
3.4 switch语句
switch
语句是一种多路判定语句,它测试表达式是否与一些整形常量中的某一个值匹配,并执行相应的分支动作。其语法如下:
1
2
3
4
5
6
7
8
9
switch (表达式) {
case 常量表达式1:
语句1
case 常量表达式2:
语句2
...
default:
语句n
}
各分支表达式必须互不相同。 如果所有分支都不匹配则执行default
分支,default
分支是可选的。如果所有分支都不匹配也没有default
分支,则switch
语句不执行任何动作。各分支及default
分支的排列次序是任意的(但习惯上将default
分支放在最后)。
在第1章曾用if-else
结构编写过一个统计字符出现次数的程序。下面用switch
语句改写该程序如下:
break
语句导致程序的执行立即从switch
语句中退出。在switch
语句中,case
的作用只是一个标号,因此某个分支的代码执行完后,程序将进入(fall through)下一分支继续执行,除非在程序中显式地跳转 。跳出switch
语句最常用的方法是break
语句和return
语句。
依次执行各分支(fall through)的做法有优点也有缺点。优点是它可以让若干个分支执行同一个动作,如上例中对数字的处理。但是,正常情况下为了防止直接进入下一个分支执行,每个分支必须以一个break
语句结束。从一个分支直接进入下一个分支的做法在程序修改时很容易出错,应尽量减少这种做法。在不得不使用的情况下应该加上适当的注释。
作为一种良好的程序设计风格,在switch
语句最后一个分支(即default
分支)后也加上一个break
语句。这样做在逻辑上没有必要,但当我们需要向末尾添加其他分支时,这种防范措施会降低犯错误的可能性。
练习3-2 编写一个函数escape(s, t)
,将字符串t复制到字符串s中,并在复制过程中将换行符、制表符等不可见字符分别转换为\n
、\t
等相应的可见的转义字符序列。要求使用switch
语句。再编写一个具有相反功能的函数,在复制过程中将转义字符序列转换为实际字符。
3.5 while循环与for循环
while
循环语句:
1
2
while (表达式)
语句
首先求表达式的值。如果其值为真(非0),则执行语句,并再次求该表达式的值。这一过程一直进行下去,直到该表达式的值为假(0)为止。
for
循环语句:
1
2
for (表达式1; 表达式2; 表达式3)
语句
等价于
1
2
3
4
5
表达式1;
while (表达式2) {
语句
表达式3;
}
但当语句中包含continue
语句时就不一定等价了。
从语法角度看,for
循环的三个组成部分都是任意表达式。最常见的情况是,表达式1与表达式3是赋值表达式或函数调用,表达式2是关系表达式。这三个组成部分中的任何部分都可以省略,但分号必须保留。 如果省略表达式1与表达式3,它就退化成了while
循环;如果省略表达式2,则认为其值永远是真。因此
1
2
3
for (;;) {
...
}
是一个“无限”循环,需要借助其他手段(如break
或return
)才能终止执行。
使用while
循环还是for
循环主要取决于个人偏好。while
循环更适合没有初始化或重新初始化操作的情况;for
循环更适合语句中需要执行简单的初始化和变量递增的情况,它将循环控制语句集中放在循环的开头,结构更紧凑、更清晰。例如,C语言处理数组前n个元素的一种习惯性用法:
1
2
for (i = 0; i < n; ++i)
...
下面的例子重新编写将字符串转换为对应数值的函数atoi
。这个版本比第2章中的atoi
函数更通用,它可以处理可选的前导空白符以及一个可选的正(+)或负(-)号。下面是程序的结构:
1
2
3
如果有空白符,则跳过
如果有符号,则读取符号
读取整数部分,并执行转换
当遇到第一个不能转换为数字的字符时,整个处理过程终止。
标准库<stdlib.h>中提供了两个更完善的函数atoi
和strtol
。
把循环控制部分集中在一起,对于多重嵌套循环,优势更为明显。下面的函数是对整型数组排序的Shell排序算法。其基本思想是:先比较距离较远的元素,而不是像简单交换排序算法那样先比较相邻的元素。这样可以快速减少大量的无序情况,从而减轻后续的工作。被比较的元素之间的距离逐步减少,直到减少为1,这时排序变成了相邻元素的互换。
逗号运算符(,
)是C语言优先级最低的运算符,在for
语句中经常会用到它。被逗号分隔的一对表达式将按照从左到右的顺序进行求值,右边表达式的类型和值即为其结果的类型和值。这样在for
语句中,可以将多个表达式放在各个部分中,比如同时处理两个循环控制变量。
以下面的函数reverse(s)
为例,该函数用于反转字符串s:
分隔函数参数的逗号、分隔声明中变量的逗号等并不是逗号运算符,不保证从左到右的求值顺序。
应该慎用逗号运算符。逗号运算符最适用于关系紧密的结构中,比如上面的reverse
函数内的for
语句,以及需要在单个表达式中进行多步计算的宏。逗号表达式还适用于reverse
函数中的元素交换,这样元素交换可以看成单步操作:
1
2
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中类似于a-z的速记符号在字符串s2中扩展为等价的完整列表abc…xyz。该函数可以处理大小写字母和数字,并可以处理a-b-c、a-z0-9和-a-z等类似的情况。前导和尾随的-字符原样保留。
3.6 do-while循环
do-while
循环的语法如下:
1
2
3
do
语句
while (表达式);
先执行语句部分,然后再求表达式的值。如果表达式的值为真,则再次执行语句,以此类推。当表达式的值变为假,则循环终止。
while
和for
循环在循环体执行前对终止条件进行测试,而do-while
循环在循环体执行后测试终止条件,因此循环体至少被执行一次。
经验表明,do-while
循环比while
和for
循环用得少得多,但有时还是很有用的,下面通过itoa
函数来说明这一点。itoa
函数是atoi
函数的逆函数,将数字转换为字符串:首先生成反序的字符串,然后再反转该字符串。
练习3-4 在数的对二的补码表示中,我们编写的itoa
函数不能处理最小的负数,即n等于-2字长-1的情况。请解释其原因。修改该函数,使它在任何机器上运行时都能打印出正确的值。
练习3-5 编写函数itob(n, s, b)
,将整数n转换为b进制数,并将结果以字符串的形式保存到s中。
练习3-6 修改itoa
函数,使得该函数可以接收第三个参数。其中,第三个参数为最小字段宽度。为了保证转换后的字符串具有第三个参数指定的最小宽度,在必要时应在结果的左边填充一定的空格。(注:类似于printf
函数中%6d
指定的宽度)
3.7 break语句与continue语句
break
语句可用于从for
、while
与do-while
循环中提前退出,如同从switch
语句中提前退出一样。break
语句能使程序从最内层循环中立即跳出。
下面的trim
函数用于删除字符串尾部的空格、制表符与换行符:
continue
语句用于使for
、while
或do-while
语句开始下一次循环的执行。在while
与do-while
语句中,这意味着立即执行测试部分;在for
语句中,则意味着使控制转移到递增循环变量部分。continue
语句只用于循环语句,不用于switch
语句。
例如,下面这段程序用于处理数组a中的非负元素:
1
2
3
4
5
for (i = 0; i < n; ++i) {
if (a[i] < 0) /* 跳过负元素 */
continue;
... /* 处理正元素 */
}
当循环后面的部分比较复杂时,常常会用到continue
语句。
3.8 goto语句与标号
C语言提供了可随意滥用的goto
语句以及标记跳转位置的标号。从理论上讲,goto
语句是没有必要的,实践中不使用goto
语句也可以很容易地写出代码。
但是,在某些场合下goto
语句还是用得着的。最常见的用法是跳出多层循环。不能直接使用break
语句,因为它只能跳出最内层循环。下面是使用goto
语句的一个例子:
1
2
3
4
5
6
7
8
9
for (...)
for (...) {
...
if (disaster)
goto error;
}
...
error:
// 处理错误情况
在该例子中,如果错误处理代码很重要,并且错误可能出现在多个地方,使用goto
语句将会比较方便。
标号和变量名的形式相同,后面跟着一个冒号。标号可位于goto
语句所在函数的任何语句前面,标号的作用域是整个函数。
所有使用了goto
语句的代码都能改写成不带goto
语句的,但可能会增加一些额外的重复测试或变量。
大多数情况下,使用goto
语句的代码比不使用goto
语句的代码要难以理解和维护,建议尽可能少地使用goto
语句。