包括
if
语句在内的某些C语句必须测试表达式的值是“真”还是“假”。例如,if
语句可能需要检测表达式i < j
,若取得真值则说明i
小于j
。在许多编程语言中,类似i < j
这样的表达式都具有特殊的“布尔”类型或“逻辑”类型。这样的类型只有两个值,即假和真。而在C语言中,i < j
这样的比较运算会产生整数:0(假)
或1(真)
。先记住这一点,下面来看看用于构建逻辑表达式的运算符。
C语言的关系运算符(relational operator)跟数学上的<
、>
、≤
和≥
运算符相对应,只不过用在C语言的表达式中时产生的结果是0(假)
或1(真)
。例如,表达式10 < 11
的值为1
,而表达式11 < 10
的值为0。
关系运算符可以用于比较整数和浮点数,也允许比较混合类型的操作数。
从优先级上来看,关系运算符的优先级低于算术运算符。
请注意,表达式
i < j < k
在C语言中是合法的,但是由于<
运算符是左结合的,因此表达式等价于(i < k) < k
,要判断j
在i
和k
之间,正确的写法应该是i < j && j < k
。
==
)!=
)和关系运算符一样,判等运算符也是左结合的,并且产生0(假)
或1(真)
作为结果。然而,判等运算符的优先级低于关系运算符。
!
)&&
)||
)逻辑运算符所产生的结果是0
或1
。逻辑运算符将任何非零值操作数作为真值来处理,同时将任何零值操作数作为假值来处理。
运算符
&&
和运算符||
都对操作数进行“短路”计算。也就是说,这些运算符首先计算出左操作数的值,然后计算右操作数。如果表达式的值可以仅由左操作数的值推导出来,那么将不计算右操作数的值。运算符
!
的优先级和一元正负号的优先级相同,运算符&&
和运算符||
的优先级低于关系运算符和判等运算符。例如,表达式i < j && k == m
等价于表达式(i < j) && (k == m)
。运算符!
是右结合的,而运算符&&
和运算符||
都是左结合的。
if
语句允许程序通过测试表达式的值从两种选项中选择一种。
if
语句的最简单格式如下:
if (表达式) 语句
其中,表达式两边的圆括号是必需的,它们是if
语句的组成部分。
执行if
语句时,先计算圆括号内表达式的值。如果表达式的值非零(C语言把非零值解释为真值),那么接着执行圆括号后边的语句。
复合语句由一对花括号,以及花括号内的声明和语句混合而成。可以有多个声明和多条语句,也可以都没有。在后一种情况下,复合语句只有一对花括号,它什么也不做。典型地,通过在一组语句周围放置花括号,可以强制编译器将其作为一条语句来处理。
下面是在if语句内部使用复合语句的形式:
if (line_num == MAX_LINES) {
line_num = 0;
page_num++;
}
复合语句也常出现在循环和其他需要多条语句(但C语言的语法要求一条语句)的地方。
if
语句可以有else
子句:
if (表达式) 语句 else 语句
如果圆括号内的表达式的值为0
,那么就执行else
后边的语句。
当在if
语句中嵌套if
语句时,记得要为语句增加花括号(即使有时并不是必需的),就像在表达式中使用圆括号一样,这两种方法都可以使程序更加容易阅读,同时可以避免出现编译器不能像程序员一样去理解程序的问题。
编程时常常需要判定一系列的条件,一旦其中某一个条件为真就立刻停止。“级联式”
if
语句常常是编写这类系列判定的最好方法。
if (表达式)
语句
else if (表达式)
语句
else if (表达式)
语句
else
语句
当然,最后两行(else
语句)不是总出现的。这种缩进级联式if
语句的方
法避免了判定数量过多时过度缩进的问题。此外,这样也向读者证明了这组语句只是一连串的判定。
请记住,级联式
if
语句不是新的语句类型,它仅仅是普通的if
语句,只是碰巧有另外一条if
语句作为else
子句(而且这条if
语句又有另外一条if
语句作为它自己的else
子句,以此类推)。
当
if
语句嵌套时,千万要当心著名的“悬空else
”的问题。思考下面这个例子:
if (y != 0)
if (x != 0)
result = x / y;
else
printf("Error: y is equal to 0\n");
上面的else
子句究竟属于哪一个if
语句呢?缩进格式暗示它属于最外层的if
语句。然而,C语言遵循的规则是else
子句应该属于离它最近的且还未和其他else
匹配的if
语句。在此例中,else
子句实际上属于最内层的if
语句,因此正确的缩进格式应该如下所示:
if (y != 0)
if (x != 0)
result = x / y;
else
printf("Error: y is equal to 0\n");
为了使else
子句属于外层的if
语句,可以把内层的if
语句用花括号括起来:
if (y != 0) {
if (x != 0)
result = x / y;
} else
printf("Error: y is equal to 0\n");
这个示例表明了花括号的作用。如果把花括号用在本节第一个示例的if
语句上,那么就不会有这样的问题了。
C语言的
if
语句允许程序根据条件的值来执行两个操作中的一个。C语言还提供了一种特殊的运算符,这种运算符允许表达式依据条件的值产生两个值中的一个。
条件运算符(conditional operator)由符号?
和符号:
组成,两个符号必须按如下格式一起使用:
表达式1? 表达式2 : 表达式3
上面的表达式可以是任何类型的表达式,按上述方式组合成的表达式称为条件表达式。条件运算符是C运算符中唯一一个要求3个操作数的运算符。因此,它通常被称为三元运算符。
条件表达式表达式1?表达式2:表达式3
读作“如果表达式1
成立,那么表达式2
,否则表达式3
”。
条件表达式也普遍用于某些类型的宏定义中。
C89标准中也没有定义布尔类型,但是有几种解决方法。
int
型变量,然后将其赋值为0
或1
:int flag;
flag = 0;
....
flag = 1;
TRUE
和FALSE
这样的名字定义宏:#define TRUE 1
#define FALSE 0
flag = FALSE;
...
flag = TRUE;
为了发扬这一思想,甚至可以定义一个可用作类型的宏:
#define BOOL int
BOOL flag;
在后面的章节中,我们将介绍一些更好的方法,可以使用类型定义( 7.5节)和枚举( 16.5 节)在C89中设置布尔类型。
C99提供了
_Bool
型,因此在C语言的这一版本中,布尔变量可以声明为
_Bool flag;
_Bool
是整数类型(更准确地说是无符号整型),因此_Bool
变量实际上就是整型变量;但是和一般的整型不同,_Bool
只能赋值为0
或1
。一般来说,往_Bool
变量中存储非零值会导致变量赋值为1
:
flag = 5; /* flag is assigned 1 */
除了_Bool
类型的定义,C99还提供了一个新的头
,这使得操作布尔值更加容易。该头提供了bool
宏,用来代表_Bool
。
#include
....
bool flag;
flag = false;
....
flag = true;
头使用起来非常方便,因此在后面的程序中需要使用布尔变量时都用到了这个头。
C语言提供了switch
语句作为级联式if
语句的替代。
if (grade == 4)
printf("Excellent");
else if (grade == 3)
printf("Good");
else if (grade == 2)
printf("Average");
else if (grade == 1)
printf("Poor");
else if (grade == 0)
printf("Failing");
else
printf("Illegal grade");
/
switch (grade) {
case 4: printf("Excellent");
break;
case 3: printf("Good");
break;
case 2: printf("Average");
break;
case 1: printf("Poor");
break;
case 0: printf("Failing");
break;
default: printf("Illegal grade");
break;
}
执行上述
switch
语句时,变量grade
的值与4
、3
、2
、1
和0
进行比较。例如,如果值和4
相匹配,那么显示信息Excellent
,然后break
语句( 6.4 节)把控制传递给switch
后边的语句。如果grade
的值和列出的任何选项都不匹配,那么执行default
分支的语句,显示消息
Illegal grade
。
switch
语句往往比级联式if
语句更容易阅读。此外,switch
语句往往比if
语句执行速度快,特别是在有许多情况要判定的时候。
switch
语句十分复杂,下面逐一看一下它的组成部分。
控制表达式。switch
后边必须跟着由圆括号括起来的整型表达式。C语言把字符(7.3节)当成整数来处理,因此在switch
语句中可以对字符进行判定。但是,这不适用于浮点数和字符串。
分支标号。每个分支的开头都有一个标号,格式如下:
case 常量表达式:
常量表达式(constant expression)很像普通的表达式,只是不能包含变量和函数调用。因此,5
是常量表达式,5 + 10
也是常量表达式,但n + 10
不是常量表达式(除非n
是表示常量的宏)。分支标号中常量表达式的值必须是整数(字符也可以)。
语句。每个分支标号的后边可以跟任意数量的语句,并且不需要用花括号把这些语句括起来。(好好享受这一点,这可是C语言中少数几个不需要花括号的地方。)每组语句的最后一条通常是break
语句。
C语言不允许有重复的分支标号,但对分支的顺序没有要求,特别是default
分支不一定要放置在最后。
case
后边只可以跟随一个常量表达式。但是,多个分支标号可以放置在同一组语句的前面:
switch (grade) {
case 4: case 3: case 2: case l:
printf("Passing");
break;
case 0: printf("Failing");
break;
default: printf("Illegal grade");
break;
}
switch
语句不要求一定有default
分支。如果default
不存在,而且控制表达式的值和任何一个分支标号都不匹配的话,控制会直接传给switch
语句后面的语句。
需要break
语句是由于switch
语句实际上是一种“基于计算的跳转”。对控制表达式求值时,控制会跳转到与switch
表达式的值相匹配的分支标号处。分支标号只是一个说明switch
内部位置的标记。在执行完分支中的最后一条语句后,程序控制“向下跳转”到下一个分支的第一条语句上,忽略下一个分支的分支标号。如果没有break
语句(或者其他某种跳转语句),控制将从一个分支继续流向下一个分支。思考下面的switch
语句:
switch (grade) {
case 4: printf("Excellent");
case 3: printf("Good");
case 2: printf("Average");
case 1: printf("Poor");
case 0: printf("Fai1ing");
default: printf("Illegal grade");
}
/*
如果grade的值为3,那么显示的消息是
GoodAveragePoorFailingIllegal grade
*/
问1:当我用
=
代替==
时,我所用的编译器没有发出警告。是否有办法可以强制编译器注意这类问题?
答:幸运的是,许多编译器可以检测出if
条件中=
运算符的可疑使用。例如,GCC会在选中-Wparentheses
选项或-Wall
(所有情况都警告)选项时执行这样的检查。GCC允许程序员通过在if
条件外面增加一对圆括号的方式来禁用该警告:
if ((i = j)) ...
问2:如果
i
是int
型变量,f
是float
型变量,那么条件表达式(i > 0 ? i: f)
是哪一种类型的值?
答:如问题所述,当int
型和float
型的值混合在一个条件表达式中时,表达式的类型为float
型。如果i > 0
为真,那么变量i
转换为float
型后的值就是表达式的值。
问3:为什么
_Bool
这个名字就不会影响已有的程序呢?
答:C89标准指出,以下划线开头,后跟一个大写字母的名字是保留字,程序员不应该使用。
循环(loop)是重复执行其他语句(循环体)的一种语句。在C 语言中,每个循环都有一个控制表达式(controlling expression)。每次执行循环体(循环重复一次)时都要对控制表达式求值。如果表达式为真(即值不为零),那么继续执行循环。
C语言提供了3种重复语句,即while
语句、do
语句和for
语句。while
循环在循环体执行之前测试控制表达式,do
循环在循环体执行之后测试控制表达式,for
语句则非常适合那些递增或递减计数变量的循环。
本章最后两节致力于讨论与循环相关的C语言特性。break
语句用来跳出循环并把程序控制传递到循环后的下一条语句,continue
语句用来跳过本次循环的剩余部分,而goto
语句则可以跳到函数内的任何语句上。
在C语言所有设置循环的方法中,
while
语句是最简单也是最基本的。while
语句的格式如下所示:
while (表达式) 语句
圆括号内的表达式是控制表达式,圆括号后边的语句是循环体。
执行while
语句时,首先计算控制表达式的值。如果值不为零(即真值),那么执行循环体,接着再次判定表达式。这个过程(先判定控制表达式,再执行循环体)持续进行,直到控制表达式的值变为零才停止。
请注意,虽然循环体必须是单独的一条语句,但这只是个技术问题;如果需要多条语句,那么只要用一对花括号构造成一条复合语句就可以了,比如:
while (i > 0) {
printf("flsdjfklsdjlkfjklsdj");
i--;
}
如果控制表达式的值始终非零(为真),while
语句将无法终止。事实上,C程序员有时故意用非零常量作为控制表达式来构造无限循环:
[惯用法] while (1) ...
除非循环体中含有跳出循环控制的语句(break
、goto
、return
)或者调用了导致程序终止的函数,否则上述形式的while
语句将永远执行下去。
小知识:如何把输出整齐地排成两列?窍门是使用类似
%10d
这样的转换说明代替%d
,并利用了printf
函数在指定宽度内输出右对齐的特性。
下面是一个利用循环进行数列求和的例子:
/* Sums a series of numbers */
#include
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;
}
/*
This program sums a series of integers.
Enter integers (0 to terminate): 8 23 71 5 0
The sum is: 107
*/
注意,条件n != 0
在数被读入后立即进行判断,这样可以尽快终止循环。此外,程序中用到了两个完全一样的scanf
函数调用,在使用while
循环时往往很难避免这种现象。
do
语句和while
语句关系紧密。事实上,do
语句本质上就是while
语句,只不过其控制表达式是在每次执行完循环体之后进行判定的。do
语句的格式如下所示:
do 语句 while (表达式)
和处理while
语句一样,do
语句的循环体也必须是一条语句(当然可以用复合语句),并且控制表达式的外面也必须有圆括号。
执行do
语句时,先执行循环体,再计算控制表达式的值。如果表达式的值是非零的,那么再次执行循环体,然后再次计算表达式的值。在循环体执行后,若控制表达式的值变为0,则终止do
语句的执行。
do
语句和while
语句往往难以区别。两种语句的区别是,do
语句的循环体至少要执行一次,而while
语句在控制表达式初始值为0
时会完全跳过循环体。
下面是一个利用do循环计算整数的位数的例子:
/* Calculates the number of digits in an integer */
#include
int main(void)
{
int digits = 0, n;
printf("Enter a nonnegative integer: ");
scanf("%d", &n);
do {
n /= 10;
digits++;
} while (n > 0);
printf("The number has %d digit(s).\n", digits);
return 0;
}
/*
Enter a nonnegative integer: 60
The number has 2 digit(s).
*/
for
语句是C语言循环中最后一种循环,也是功能最强大的一种循环。不要因为for
语句表面上的复杂性而灰心;实际上,它是编写许多循环的最佳方法。for
语句非常适合应用在使用“计数”变量的循环中,当然它也可以灵活地用于许多其他类型的循环中。格式如下:
for (声明或者表达式1;表达式2;表达式3) 语句
其中表达式1、表达式2 和表达式3全都是表达式。下面是一个例子:
for (i = 10; i > 0; i--)
printf("T minus %d and counting\n",i);
/*
在执行for语句时,变量i先初始化为10,接着判定i是否大于0。因为判定的结果为真,所以打印信息T minus 10 and counting,然后变量i进行自减操作。随后再次对条件i > 0进行判定。循环体总共执行10次,在这一过程中变量i从10变化到1。
*/
for
语句和while
语句关系紧密。事实上,除了一些极少数的情况以外,for
循环总可以用等价的while
循环替换:
表达式1;
while (表达式2) {
语句
表达式3;
}
对于“向上加”(变量自增)或“向下减”(变量自减)的循环来说,
for
语句通常是最好的选择。对于向上加或向下减共n
次的情况,for语句经常会采用下列形式中的一种。
0
向上加到n-1
:for ( i = 0; i < n; i++) ...
1
向上加到n
:for ( i = 1; i <= n; i++) ...
n-1
向下减到0
:for ( i = n-1; i >= 0; i--) ...
n
向下减到1
:for ( i = n; i > 0; i--) ...
模仿上面的书写格式有助于避免C语言初学者常犯的下列错误:
>
写成<
(或者相反)。注意,“向上加”的循环使用运算符<
或运算符<=
,而“向下减”的循环则依赖于运算符>
或运算符>=
。<
、<=
、>
或>=
写成==
。控制表达式的值在循环开始时应该为真,以后会变为假以便能终止循环。类似i == n
这样的判定没什么意义,因为它的初始值不为真。i < n
写成i <= n
,这会犯“循环次数差一”错误。通常for
语句用三个表达式控制循环,但是有一些for
循环可能不需要这么多,因此C语言允许省略任意或全部的表达式。
注意,保留第一个表达式和第二个表达式之间的分号。即使省略掉某些表达式,控制表达式也必须始终有两个分号。
某些程序员用下列
for
语句建立无限循环:for (;;) ...
在C99中,for
语句的第一个表达式可以替换为一个声明,这一特性使得程序员可以声明一个用于循环的变量:
for (int i = 0; i < n; i++)
变量
i
不需要在该语句前进行声明。事实上,如果变量i
在之前已经进行了声明,这个语句将创建一个新的i
且该值仅用于循环内。
for
语句声明的变量不可以在循环外访问(在循环外不可见)。顺便提一下,for
语句可以声明多个变量,只要它们类型相同。
for (int i = 0, j = 0; i < n; i++)
....
有些时候,我们可能喜欢编写有两个(或更多个)初始表达式的
for
语句,或者希望在每次循环时一次对几个变量进行自增操作。使用逗号表达式(comma expression)作为for
语句中第一个或第三个表达式可以实现这些想法。逗号表达式的格式如下所示:
表达式1, 表达式2
这里的表达式1
和表达式2
是两个任意的表达式。逗号表达式的计算要通过两步来实现:
表达式1
并且扔掉计算出的值;表达式2
,把这个值作为整个表达式的值。对表达式1
的计算应该始终会有副作用;如果没有,那么表达式1
就没有了存在的意义。i = 1, j = 2, k = i + j
/*
因为逗号表达式的左操作数在右操作数之前求值,所以赋值运算i = 1、j = 2 和k = i + j是从左向右进行的。
*/
提供逗号运算符是为了在C语言要求只能有一个表达式的情况下,可以使用两个或多个表达式。换句话说,逗号运算符允许将两个表达式“粘贴”在一起构成一个表达式。(注意它与复合语句的相似之处,后者允许我们把一组语句当作一条语句来使用。)
需要把多个表达式粘在一起的情况不是很多。正如后面的某一章将介绍的那样,某些宏定义( 14.3 节)可以从逗号运算符中受益。除此之外,
for
语句是唯一可以发现逗号运算符的地方。
然而,有些时候也需要在循环中间设置退出点,甚至可能需要对循环设置多个退出点。break
语句可以用于有上述这些需求的循环中。
continue
语句会跳过某次迭代的部分内容,但是不会跳出整个循环。goto
语句允许程序从一条语句跳转到另一条语句。因为已经有了break
和continue
这样有效的语句,所以很少使用goto
语句。
break
语句把程序控制从switch
语句中转移出来,还可以用于跳出while
、do
或for
循环。
值得注意的是,
break
语句把程序控制从包含该语句的最内层while
、do
、for
或switch
语句中转移出来。因此,当这些语句出现嵌套时,break
语句只能跳出一层嵌套。
while (...) {
switch (...) {
...
break;
...
}
}
/*
break语句可以把程序控制从switch语句中转移出来,
但是不能跳出while循环。
*/
break
语句刚好把程序控制转移到循环体末尾之后,而continue
语句刚好把程序控制转移到循环体末尾之前。break
语句会使程序控制跳出循环,而continue
语句会把程序控制留在循环内。break
语句和continue
语句的另外一个区别是,break
语句可以用于switch
语句和循环(while
、do
和for
),而continue
语句只能用于循环。
goto
语句在早期编程语言中很常见,但在日常C语言编程中已经很少用到它了。
break
、continue
、return
语句(本质上都是受限制的goto
语句)和exit
函数( 9.5 节)足以应付在其他编程语言中需要goto
语句的大多数情况。
goto
语句则可以跳转到函数中任何有标号的语句处。[C99增加了一条限制:goto
语句不可以用于绕过变长数组( 8.3 节)的声明。]
标识符 : 语句
goto 标识符
执行语句goto L;
,控制会转移到标号L
后面的语句上,而且该语句必须和goto
语句在同一个函数中。
while (...) {
switch (...) {
...
goto loop_done; /* break won’t work here */
...
}
}
loop_done: ...
//goto语句对于嵌套循环的退出也是很有用的。
语句可以为空,也就是除了末尾处的分号以外什么符号也没有。下面是一个示例:
i = 0; ; j = 1;
这行含有三条语句:一条语句是给i
赋值,一条是空语句,还有一条是给j
赋值。
空语句主要有一个好处:编写空循环体的循环。(注意空语句单独放置在一行。)
for (d = 2; d < n && n % d != 0; d++)
/* empty loop body */;
C程序员习惯性地把空语句单独放置在一行。否则,有些人阅读程序时可能会搞不清for
语句后边的语句是否是其循环体。
请注意!如果不小心在
if
、while
或for
语句的圆括号后放置分号,则会创建空语句,从而造成if
、while
或for
语句提前结束。
问1:
while (i > 0) printf("T minus %d and counting\n", i--); 为什么不删除“> 0”判定来进一步缩短循环呢? while (i) printf("T minus %d and counting\n", i--); 这种写法的循环会在i 达到0 值时停止,所以它应该和原始版本一样好。
答:新写法确实更加简洁,许多C程序员也这样写循环。但是,它也有缺点。
首先,新循环不像原始版本那样容易阅读。新循环可以清楚地显示出在i
达到0
值时循环终止,但是不能清楚地表示是向上计数还是向下计数。而在原始的循环中,根据控制表达式i > 0
可以推断出这一信息。
其次,如果循环开始执行时i
碰巧为负值,那么新循环的行为会不同于原始版本。原始循环会立刻终止,而新循环则不会。
问2:6.3节提到,大多数
for
循环可以利用标准模式转换成while
循环。能给出一个反例吗?
答:当for
循环体中含有continue
语句时,6.3节给出的while
模式将不再有效。
问3:哪个无限循环格式更可取,
while(1)
还是for(;;)
?
答:C程序员传统上喜欢for(;;)
的高效性。这是因为早期的编译器经常强制程序在每次执行while
循环体时测试条件1
。但是,对于现代编译器来说,在性能上两种无限循环应该没有差别。
问4:听说程序员应该永不使用
continue
语句。这种说法对吗?
答:continue
语句的确很少使用。尽管如此,continue
语句有时还是非常方便的。假设我们编写的循环要读入一些输入数据并测试其有效性,如果有效则以某种方法进行处理。如果有许多有效性测试,或者如果它们都很复杂,那么continue
语句就非常有用了。循环将类似于下面这样:
for(; ;) {
读入数据;
if(数据的第一条测试失败)
continue;
if(数据的第二条测试失败)
continue; . . .
if(数据的最后一条测试失败)
continue;
处理数据;
}
问5:
goto
语句有什么不好?
答:goto
语句不是天生的魔鬼,只是通常它有更好的替代方式。使用过多goto
语句的程序会迅速退化成“垃圾代码”,因为控制可以随意地跳来跳去。垃圾代码是非常难于理解和修改的。
因为goto
语句既可以往前跳又可以往后跳,所以使得程序难于阅读。(break
语句和continue
语句只是往前跳。)含有goto
语句的程序经常要求阅读者来回跳转以理解代码的控制流。
goto
语句使程序难于修改,因为它可能会使某段代码用于多种不同的目的。例如,对于前面有标号的语句,既可以在执行完其前一条语句后到达,也可以通过多条goto
语句中的一条到达。
问6:除了说明循环体为空外,空语句还有其他用途吗?
答:非常少。空语句可以放在任何允许放语句的地方,所以有许多潜在的用途。但在实际中,空语句只有一种别的用途,而且极少使用。
问7:除了把空语句单独放置在一行以外,是否还有其他方法可以凸显出空循环体?
答:一些程序员使用虚设的continue
语句:
for (d = 2; d < n && n % d != 0; d++)
continue;
还有一些人使用空的复合语句:
for (d = 2; d < n && n % d != 0; d++)
{}
本文是作者阅读《C语言程序设计:现代方法(第2版·修订版)》时所做笔记,日后会持续更新后续章节笔记。欢迎各位大佬批评指正,希望对诸位有所帮助,Thank you very much!