if语句
目前我们写的简单函数中可以有多条语句,但这些语句总是从前到后顺序执行的。除了顺序执行之
外,有时候我们需要检查一个条件,然后根据检查的结果执行不同的后续代码,在C语言中可以用
分支语句(Selection Statement)实现,比如:
if (x != 0) { printf("x is nonzero.\n"); }
其中 x != 0 表示 “x 不等于 0” 的条件,这个表达式称为控制表达式( Controlling Expression )如果条
件成立,则 {} 中的语句被执行,否则 {} 中的语句不执行,直接跳到 } 后面。 if 和控制表达式改变了程
序的控制流程( Control Flow ),不再是从前到后顺序执行,而是根据不同的条件执行不同的语
句,这种控制流程称为分支( Branch )。上例中的 != 号表示 “ 不等于 ” ,像这样的运算符有:
关系运算符和相等性运算符
运算符 | 含义 |
== | 等于 |
!= | 不等于 |
> | 大于 |
< | 小于 |
>= | 大于或等于 |
<= | 小于或等于 |
注意以下几点:
1.这里的 == 表示数学中的相等关系,相当于数学中的 = 号,初学者常犯的错误是在控制表达式中把== 写成 = ,在 C 语言中 = 号是赋值运算符,两者的含义完全不同。
2.如果表达式所表示的比较关系成立则值为真( True ),否则为假( False ),在 C 语言中分别
用 int 型的 1 和 0 表示。如果变量 x 的值是 -1 ,那么 x>0 这个表达式的值为 0 , x>-2 这个表达式的
值为 1。
3.在数学中a
左结合的,请读者想一下这个表达式应如何求值。
4.这些运算符的两个操作数应该是相同类型的,两边都是整型或者都是浮点型可以做比较,但
两个字符串不能做比较,在后面我们会介绍比较字符串的方法。
5.==和!=称为相等性运算符(Equality Operator),其余四个称为关系运算符(Relational
Operator),相等性运算符的优先级低于关系运算符。
总结一下, if (x != 0) { ... } 这个语句的计算顺序是:首先求 x != 0 这个表达式的值,如果值为0 ,就跳过 {} 中的语句直接执行后面的语句,如果值为 1 ,就先执行 {} 中的语句,然后再执行后面的语句。事实上控制表达式取任何非0 值都表示真值,例如 if (x) { ... } 和 if (x != 0) { ...}是等价的,如果 x 的值是 2 ,则 x != 0 的值是 1 ,但对于 if 来说不管是 2 还是 1 都表示真值。
和 if 语句相关的语法规则如下:
语句 → if ( 控制表达式 ) 语句
语句 → { 语句列表}
语句 → ;
在 C 语言中,任何允许出现语句的地方既可以是由 ; 号结尾的一条语句,也可以是由 {} 括起来的若干
条语句或声明组成的语句块( Statement Block ),语句块和上一章介绍的函数体的语法相同。注意语句块的} 后面不需要加 ; 号。如果 } 后面加了 ; 号,则这个 ; 号本身又是一条新的语句了,在 C 语言中一个单独的; 号表示一条空语句( Null Statement )。上例的语句块中只有一条语句,其实没必要写成语句块,可以简单地写成:
if (x != 0) printf("x is nonzero.\n");
语句块中也可以定义局部变量,例如:
void foo(void) { int i = 0; { int i = 1; int j = 2; printf("i=%d, j=%d\n", i, j); } printf("i=%d\n", i); /* cannot access j here */ }
和函数的局部变量同样道理,每次进入语句块时为变量 j 分配存储空间,每次退出语句块时释放变
量 j的存储空间。语句块也构成一个作用域,和 作用域的分析类似,如果整个源文件是一张
大纸, foo 函数是盖在上面的一张小纸,则函数中的语句块是盖在小纸上面的一张更小的纸。语句
块中的变量 i 和函数的局部变量 i 是两个不同的变量,因此两次打印的 i 值是不同的;语句块中的变
量 j 在退出语句块之后就没有了,因此最后一行的 printf 不能打印变量 j ,否则编译器会报错。语句
块可以用在任何允许出现语句的地方,不一定非得用在 if 语句中,单独使用语句块通常是为了定义
一些比函数的局部变量更 “ 局部 ” 的变量。
if/else语句
if语句还可以带一个else子句(Clause),例如:
if (x % 2 == 0) printf("x is even.\n"); else printf("x is odd.\n");
这里的 % 是取模( Modulo )运算符, x%2 表示 x 除以 2 所得的余数( Remainder ), C 语言规定 % 运
算符的两个操作数必须是整型的。两个正数相除取余数很好理解,如果操作数中有负数,结果应该
是正是负呢? C99 规定,如果 a 和 b 是整型, b 不等于 0 ,则表达式 (a/b)*b+a%b 的值总是等于 a,再结表达式讲过的整数除法运算要Truncate Toward Zero,可以得到一个结论: %运算符的结果总是与被除数同号(想一想为什么)。其它编程语言对取模运算的规定各不相同,也有规定结果和除数同号的,也有不做明确规定的。
取模运算在程序中是非常有用的,例如上面的例子判断 x 的奇偶性( Parity ),看 x 除以 2 的余数是不
是 0 ,如果是 0 则打印 x is even. ,如果不是 0 则打印 x is odd. ,读者应该能看出 else 在这里的作用
了,如果在上面的例子中去掉 else ,则不管 x 是奇是偶, printf("x is odd.\n"); 总是执行。为了
让这条语句更有用,可以把它封装( Encapsulate )成一个函数:
void print_parity(int x) { if (x % 2 == 0) printf("x is even.\n"); else printf("x is odd.\n"); }
把语句封装成函数的基本步骤是:把语句放到函数体中,把变量改成函数的参数。这样,以后要检
查一个数的奇偶性只需调用这个函数而不必重复写这条语句了,例如:
print_parity(17); print_parity(18);
if/else 语句的语法规则如下:
语句 → if ( 控制表达式 ) 语句 else 语句
右边的 “ 语句 ” 既可以是一条语句,也可以是由 {} 括起来的语句块。一条 if 语句中包含一条子语句,
一条 if/else 语句中包含两条子语句,子语句可以是任何语句或语句块,当然也可以是另外一
条 if 或 if/else 语句。根据组合规则, if 或 if/else 可以嵌套使用。例如可以这样:
if (x > 0) printf("x is positive.\n"); else if (x < 0) printf("x is negative.\n"); else printf("x is zero.\n");
也可以这样:
if (x > 0) { printf("x is positive.\n"); } else { if (x < 0) printf("x is negative.\n"); else printf("x is zero.\n"); }
现在有一个问题,类似 if (A) if (B) C; else D; 形式的语句怎么理解呢?可以理解成
if (A) if (B) C; else D;
也可以理解成
if (A) if (B) C; else D;
在 第 1 节 “ 继续 Hello World” 讲过, C 代码的缩进只是为了程序员看起来方便,实际上对编译器不起
任何作用,你的代码不管写成上面哪一种缩进格式,在编译器看起来都是一样的。那么编译器到底
按哪种方式理解呢?也就是说, else 到底是和 if (A) 配对还是和 if (B) 配对?很多编程语言的语法
都有这个问题,称为 Dangling-else 问题。 C 语言规定, else 总是和它上面最近的一个 if 配对,因此
应该理解成 else 和 if (B) 配对,也就是按第二种方式理解。如果你写成上面第一种缩进的格式就很
危险了:你看到的是这样,而编译器理解的却是那样。如果你希望编译器按第一种方式理解,应该
明确加上 {} :
if (A) { if (B) C; } else D;
顺便提一下,浮点型的精度有限,不适合用 == 运算符做精确比较。以下代码可以说明问题:
double i = 20.0; double j = i / 7.0; if (j * 7.0 == i) printf("Equal.\n"); else printf("Unequal.\n");
不同平台的浮点数实现有很多不同之处,在我的平台上运行这段程序结果为 Unequal ,即使在你的
平台上运行结果为 Equal ,你再把 i 改成其它值试试,总有些值会使得结果为 Unequal 。等学习了 第 4 节 “ 浮点数 ” 你就知道为什么浮点型不能做精确比较了。
布尔代数
在第1 节“if语句”讲过,a 不表示b既大于a又小于c,那么如果想表示这个含义该怎么写呢?可
以这样:
if (a < b) { if (b < c) { printf("b is between a and c.\n"); } }
我们也可以用逻辑与( Logical AND )运算符表示这两个条件同时成立。逻辑与运算符在 C 语言中写成两个& 号( Ampersand ),上面的语句可以改写为:
if (a < b && b < c) { printf("b is between a and c.\n"); }
对于 a < b && b < c 这个控制表达式,要求 “ a < b 的值非 0” 和 “ b < c 的值非 0” 这两个条件同时成立
整个表达式的值才为 1 ,否则整个表达式的值为 0 。也就是只有两个条件都为真,它们做逻辑与运算
的结果才为真,有一个条件为假,则逻辑与运算的结果为假,如下表所示:
A | B | A AND B |
0 | 0 | 0 |
0 | 1 | 0 |
1 | 0 | 0 |
1 | 1 | 1 |
这种表称为真值表( Truth Table )。注意逻辑与运算的操作数以非 0 表示真以 0 表示假,而运算结果
以 1 表示真以 0 表示假(类型是 int ),我们忽略这些细微的差别,在表中全部以 1 表示真以 0 表示 假。C 语言还提供了逻辑或( Logical OR )运算符,写成两个 | 线( Pipe Sign ),逻辑非( Logical
NOT )运算符,写成一个 ! 号( Exclamation Mark ),它们的真值表如下:
A | B | A OR B |
0 | 0 | 0 |
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 1 |
逻辑或表示两个条件只要有一个为真,它们做逻辑或运算的结果就为真,只有两个条件都为假,逻
辑或运算的结果才为假。逻辑非的作用是对原来的逻辑值取反,原来是真的就是假,原来是假的就
是真。逻辑非运算符只有一个操作数,称为单目运算符( Unary Operator ),以前讲过的加减乘 除、赋值、相等性、关系、逻辑与、逻辑或运算符都有两个操作数,称为双目运算符(Binary
Operator )。
关于逻辑运算的数学体系称为布尔代数( Boolean Algebra),以它的创始人布尔命名。在编程语
言中表示真和假的数据类型叫做布尔类型,在 C 语言中通常用 int 型来表示,非 0 表示真, 0 表示假[ 6 ] 。布尔逻辑是写程序的基本功之一,程序中的很多错误都可以归因于逻辑错误。以下是一些布尔代数的基本定理,为了简洁易读,真和假用1 和 0 表示, AND 用 * 号表示, OR 用 + 号表示(从真值表可以看出AND 和 OR 运算确实有点像乘法和加法运算), NOT 用 ¬ 表示,变量 x 、 y 、 z 的值可能是0 也可能是 1 。
除了第1行之外,这些公式都是每两行一组的,每组的两个公式就像对联一样:把其中一个公式中 的*换成+、+换成*、0换成1、1换成0,就变成了与它对称的另一个公式。这些定理都可以通过真 值表证明,更多细节可参考有关数字逻辑的教材,例如[数字逻辑基础]。
目前为止介绍的这些运算符的优先级顺序是: ! 高于 * / % ,高于 + - ,高于 > < >= <= ,高于 == != ,
高于 && ,高于 || ,高于 = 。写一个控制表达式很可能同时用到这些运算符中的多个,如果记不清楚
运算符的优先级一定要多套括号。我们将在 运算符总结 总结 C 语言所有运算符的优先级和结合性。
switch语句
switch语句可以产生具有多个分支的控制流程。它的格式是:
switch (控制表达式) {
case 常量表达式: 语句列表
case 常量表达式: 语句列表
...
default: 语句列表
}
例如以下程序根据传入的参数1~7分别打印Monday~Sunday:
如果传入的参数是 2 ,则从 case 2 分支开始执行,先是打印相应的信息,然后遇到 break 语句,它的
作用是跳出整个 switch 语句块。 C 语言规定各 case 分支的常量表达式必须互不相同,如果控制表达
式不等于任何一个常量表达式,则从 default 分支开始执行,通常把 default 分支写在最后,但不是
必须的。使用 switch 语句要注意几点:
1.case后面跟表达式的必须是常量表达式,这个值和全局变量的初始值一样必须在编译时计算
出来。
2. 第 2 节 “if/else 语句 ”讲过浮点型不适合做精确比较,所以C语言规定case后面跟的必须是 整型
常量表达式。
3.进入case后如果没有遇到break语句就会一直往下执行,后面其它case或default分支的语句
也会被执行到,直到遇到 break ,或者执行到整个 switch 语句块的末尾。通常每个 case 后面都
要加上 break 语句,但有时会故意不加 break 来利用这个特性,例如:
switch 语句不是必不可缺的,显然可以用一组 if ... else if ... else if ... else ... 代替,
但是一方面用 switch 语句会使代码更清晰,另一方面,有时候编译器会对 switch 语句进行整体优
化,使它比等价的 if/else 语句所生成的指令效率更高。