It is easier to write an incorrect program than understand a correct one. 1
[TOC]
前面已经讲过C语言的语句主要分为 6 大类。本节我们主要探讨 选择语句:if 语句 和 switch 语句
包括 if 语句在内的某些 C 语句(while,for 等)都必须测试表达式的值是“真”还是“假”。
许多编程语言中,类似 i < j
这样的表达式都具有特殊的“布尔”类型或者“逻辑”类型(C++ 的 bool 和 Java 的 boolean)。这样的类型只有两个值,即真(true)和假(false)。
而在 C 语言中,诸如 i < j
这样的比较会产生整数:0(假)1(真)。
但是,非 0 的其他数也可以表示 真。在今天看来,这是 C 语言设计的弊端,它将布尔类型与整型混为一谈,让我们在变成过程中可能稍不小心就会给自己挖一个坑。
C 语言的关系运算符(relational operator)和数学上的
>,<,≤,≥
相对应,只是用在 C 语言的表达式中时产生的结果是 0 或 1 。例如,表达式 10 < 11 的值是 1,11 < 10 的值是 0 。
关系运算符也可以用于比较整数和浮点数,也允许比较不同类型的操作数。如:5.6 < 5 的值为 0 。
符号 | 含义 |
---|---|
< | 小于 |
> | 大于 |
<= | 小于等于 |
>= | 大于等于 |
关系运算符的优先级低于算数运算符。例如:i + j < k - 1
的意思是 (i + j) < (k - 1)
关系运算符都是左结合的。
注意: 表达式 i < j < k
在 C 语言中是合法的,但是可能不是你所期望的含义。因为 < 运算符是左结合的,所以这个表达式等价于:(i < j) < k
表达式会先检测 i 是否小于 j,然后用比较后产生的结果(1 或 0 )来和 k 进行比较。这个表达式并不是测试 j 是否位于 i 和 k 之间。(正确的写法是:j > i && j < k
)
**判等运算符(equality operator):**相等用
==
表示 。注意不是=
,=
表示赋值。
注意:
一定要注意不要将 ==
写成 =
,编译器可能会给你报错,但是如果没有,在你查错的时候,注意是不是 ==
写错了的问题。
符号 | 含义 |
---|---|
== | 等于 |
!= | 不等于 |
和关系运算符一样,判等运算符是左结合的,也是产生 0(假) 或 1(真) 作为结果。
判等运算符的优先级低于关系运算符。例如:
i < j == j < k
等价于:(i < j) == (j < k)
,含义是:如果 i < j 和 j < k 同真或同假 这个表达式的结果为真。
利用 关系运算符 和 判等运算符:
(i >= j) + (i == j)
上面表达式的值为 0,1 或者 2 分别代表 i < j, i > j, i == j
逻辑运算符(logical operator)
符号 | 含义 |
---|---|
! | 逻辑非(一元运算符) |
&& | 逻辑与(二元运算符) |
|| | 逻辑或(二元运算符) |
其实 && 就是 数学中的 且 ,|| 就是数学的的 或
逻辑运算符产生的结果仍然是 0 或 1,操作数经常也是 0 或 1,但这不是必需的。逻辑运算符将任何 非0值操作数当作真来处理,任何0值操作数当作假来处理。
- 如果表达式的值为 0,那么
!表达式
的结果为 1 - 如果 表达式1 和 表达式2 的值都是非零值,那么
表达式1 && 表达式2
的结果为 1 - 如果 表达式1 和 表达式2 的值 中的任意一个是(或者两个都是)非零值,那么
表达式1 || 表达式2
的结果为 1
所有其他情况下,这些运算符产生的结果都为 0
“短路”计算
&& 和 || 运算符都遵循“短路”原则。也就是说,这些运算符首先计算出左操作数的值,然后计算右操作数;如果表达式的值可以仅由左操作数推导出来,那么将不计算右操作数的值。如:
int i = 0, j = 1;
if (i && i++) {
; // 空语句
}
printf("%d\n", i); // i 的值没有增加,说明 i++ 没有计算
if (j || j++) {
;
}
printf("%d\n", j); // j 的值没有增加, 说明 j++ 没有计算
// 输出:
0
1
运算符 !的优先级和一元正负号优先级相同,运算符 && 和 || 的优先级低于判等运算符。
例如:i < j && k == m
等价于 (i < j) && (k == m)
运算符 ! 是右结合的,&& 和 || 是左结合的。
if 语句允许程序通过测试表达式的值从两种选项中选择一种。if 语句的简单格式如下:
if(表达式){
语句
}
如果语句部分只有一条语句,也可以写成
if(表达式)
语句;
执行 if 语句时,先计算圆括号内表达式的值。如果表达式的值非零(C语言将非零值解释为真值),那么接着执行大括号内的语句。例如:
if(i > 0)
printf("正数\n");
为了判定 k < i < j,可以这样写:
if(i > k && i < j)
为了判定相反的情况,可以写成:
if(i <= k || i >= j)
**例2-1:**程序:为了判定一个数是不是大于零的,如果是,我们就输出提示语,然后让这个数加 1
if(i > 0){
printf("是正数\n");
i++;
}
if(表达式)
语句;
else
语句;
如果是复合语句(compound statement),需要加上花括号
加上花括号是一种好习惯。建议不管是不是复合语句,尽量都加上花括号。
例2-2:增加需求:如果这个数不是正数,那么输出提示语,然后让这个数减 1
if(i > 0){
printf("是正数\n");
i++;
}else{
printf("不是正数\n");
i--;
}
例2-3:找出 i,j,k 中的最大值,并将其保存到 max 中
if(i > j){
if(i > k){
max = i;
}else{
max = k;
}
}else{
if(j > k){
max = j;
}else{
max = k;
}
}
编程时常常需要判定一系列的条件,一旦其中某个条件为真就立刻停止。
如何做到呢?
例2-4 程序:判断 n 是大于 0 还是 等于 0 还是小于 0
使用 if else
if(n < 0){
printf("n < 0");
}else{
if(n == 0){
printf("n = 0");
}
else{
printf("n > 0");
}
}
使用 else if
if(n < 0){
printf("n < 0");
}
else if(n == 0){
printf("n == 0");
}
else{
printf("n > 0");
}
这样写可以避免 if else 嵌套,从而提高了书写和理解的难易度。
级联式 if 语句书写形式:
if(表达式){
语句;
}
else(表达式){
语句;
}
else{
语句;
}
请看下面的程序,思考 else 与 那个 if 匹配
if(y != 0)
if(x != 0)
printf("%.2f", x / y);
else
printf("Error: y is zero!");
如果此时 y = 0, x = 2 会输出什么?
如果 y = 2, x = 0 会输出什么?
虽然缩进格式按时 else 属于外层 if,但是 C 语言遵循的规则是else 子句应该属于离它最近且还未和其他 else 匹配的 if 语句。
所以,此例中 else 属于内层的 if 语句。为了避免这种问题,最好的办法就是加括号。
if(y != 0){
if(x != 0)
printf("%.2f", x / y);
}
else
printf("Error: y is zero!");
**条件运算符(conditional operator):**C 语言运算符中唯一一个三元(ternary)(三个操作数)运算符。
格式:
[条件表达式]表达式 1 ? 表达式2 :表达式3 ;
例2-6
if(x > 0){
x++;
}
else{
x--;
}
上面的程序我们用条件运算符可以这么写:
x > 0 ? x++ : x--;
判断 k 的值
i = 1;
j = 2;
k = i > j ? i : j; // k is 2 now
k = (i >= 0 ? i : 0) + j; // k is now 3
条件运算符使程序更短小但也更难以阅读,所以最好避免使用。然而有的情况会常常使用条件表达式。比如:
1)判断返回:
return i > j ? i : j;
2)printf
printf("%d\n", i > j ? i : j);
条件表达式也普遍应用于某些类型的宏定义中。
多年以来,C语言一直缺乏适当的布尔类型。
一种解决方法是,先声明一个 int 型变量,让后将其赋值为 0 或 1:
int flag;
flag = 0; // 表示 flag 为 false
...
flag = 1; // 表示 flag 为 true
虽然这种方法可行,但是对于程序的可读性没有多大贡献,因为没有明确的表示 flag 的赋值只能是布尔型,并没有明确指出 0 和 1就是表示真和假。
为了使得程序更加便于理解,C89 程序员通常使用 TRUE 和 FALSE 这样的名字定义宏:
#define TRUE 1
#define FALSE 0
现在对 flag 的赋值就有了更加自然的形式:
flag = FALSE;
...
flag = TRUE;
为了判定 flag 是否为真,可以用:
if(flag == TRUE){
...
}
或者只写:
if(flag){
...
}
为了发扬这一思想,我们可以进一步定义一个用作布尔类型的宏:
#define BOOL int;
声明布尔类型变量时可以用 BOOL 替代 int
BOOL flag;
现在我们就非常清楚了:flag 不是一个普通的整型变量,而是表示布尔条件。(当然编译器还是将 flag 当作 int 类型的变量。)
C99 中提供了 _Bool 类型:
_Bool flag;
_Bool
是无符号整型。但是和一般的整形不同,_Bool 只能被赋值为 0 或 1 。一般来说,向 _Bool 类型变量中储存非零值会导致变量赋值为 1 。
_Bool flag = 5;
printf("%u", flag);
// 输出:
1
C99 还提供了一个新的头文件<stdbool.h>
,改头提供了 bool 宏,用来代表 _Bool ;还提供了 true 和 false 两个宏,分别代表 1 和 0 。于是可以写:
#include<stdbool.h>
bool flag;
flag = true;
...
flag = false;
日常的编程中,常常需要把表达式和一系列值进行比较,从而找出当前匹配的值。
使用级联式 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");
C 语言提供了 switch 语句作为这类级联式 if 语句的替换。使用 switch 语句改写上面的程序:
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 语句常用格式:
switch(控制表达式){
case 常量表达式 : 语句
...
case 常量表达式 : 语句
default : 语句
}
-
**控制表达式:**控制表达式只能用:整型,字符型的变量(C 语言把字符当成整数来处理),不能用浮点数 和 字符串。
-
**分支标号:**每一个分支的开头都有一个标号,格式如下:
case 常量表达式;
常量表达式(constant expression): 必须是整数或字符型,不能包含变量和函数调用。
5 是常量表达式,5 + 10 也是常量表达式;但是 10 + n 不是常量表达式(除非 n 是表示常量的宏)。
-
**语句:**每个分支标号后可以跟任意数量的语句。不需要用花括号把这些语句括起来。每组语句的最后一条通常是 break 语句。
-
break 的作用: 本节后面会详细讨论。
-
default 语句的作用: 控制表达式的值和所有的标号语句都不匹配的话,会执行 default 后面的语句。(default :默认的意思)
C 语言不允许有重复的分支标号,但对分支的顺序没有要求,特别是 default 分支不一定要放在最后。
case 后只可以跟随一个常量表达式。但是,多个分支标号可以放置在同一组语句前面 。如:
switch(grade){
case 4:
case 3:
case 2:
case 1: printf("Passing");
break;
case 0: printf("Failing");
break;
default:printf("Illegal grade");
break;
}
为了节省空间,可以将拥有相同语句的分支标号放在同一行:
switch(grade){
case 4: case 3: case 2: case 1:
printf("Passing");
break;
case 0: printf("Failing");
break;
default:printf("Illegal grade");
break;
}
switch 语句不要求一定有 default 分支。如果 default 不存在,而且控制表达式的值和所有的标号语句都不匹配的话,控制会直接传给 switch 语句后面的语句。
break 会使程序“跳出” switch 语句,继续执行 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("Failing");
default:printf("Illegal grade");
}
如果 grade 的值为 3,那么显示的信息是:
GoodAveragePoorFailingIllegal grade
**注意:**忘记 break 是编程时常见的错误。虽然有时候会故意忽略 break 以便多个分支共享代码,但是通常情况下省略 break 是因为忽略。
如果有需要,明确指出故意省略 break 语句是一个好主意:
switch(grade){
case 4: case 3: case 2: case 1:
num_passing++;
// Fail Through
case 0: total_grades++;
break;
}
最然 switch 语句最后一个分支不需要 break 语句,但是通常还是会加上一个 break 语句在那里,以防止将来增加分支时出现“丢失” break 的问题。
参考资料:《C Primer Plus》《C语言程序设计:现代方法》
Footnotes
-
写错误的程序比理解正确的程序简单。Epigrams on Programming 编程警句 ↩