c-programming-chap4

选择结构程序设计

It is easier to write an incorrect program than understand a correct one. [1]

if_switch

选择结构示例

求解一元二次方程

解题思路: 首先要知道求方程式的根的方法。由数学知识已知: 如果b2-4ac≥0,则一元二次方程有两个实根:𝑥1=(−𝑏+√(𝑏2−4𝑎𝑐))/2𝑎,𝑥2=(−𝑏−√(𝑏2−4𝑎𝑐))/2𝑎,将分式分为两项:p=(−𝑏)/2𝑎,q=√(𝑏^2−4𝑎𝑐)/2𝑎,则x1=p+q,x2=p-q,有了这些式子,只要知道a,b,c的值,就能顺利地求出方程的两个根。

在上述程序中进行改进。题目要求解得ax^2+bx+c=0方程的根。由键盘输入a,b,c。假设a,b,c的值任意,并不保证b2-4ac≥0。需要在程序中进行判别,如果b2-4ac≥0,就计算并输出方程的两个实根,如果b2-4ac<0,就输出“此方程无实根”的信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26


#include <stdio.h>
#include <math.h> // sqrt函数在此头文件中定义
int main(void)
{
int a, b, c;
double delta;
double x1, x2;
printf("输入a, b, c(a不为0,数据间以空格隔开):"); // 1 3 -14
scanf("%d %d %d", &a, &b, &c);
delta = b * b - 4 * a * c;
if(delta >= 0)
{
x1 = (-b + sqrt( delta ))/(2.0 * a);
x2 = (-b - sqrt( delta ))/(2.0 * a);
printf("%.3f %.3f\n",x1,x2 );
int bp_check = x1;
return bp_check;
}
else
{ printf("方程无实根。\n"); }
return 0;
}


选择语句

前面已经讲过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 同真或同假 这个表达式的结果为真。

关系运算符优先顺序

WX20220304-233448

二 逻辑运算符

逻辑运算符(logical operator)

符号 含义
! 逻辑非(一元运算符)
&& 逻辑与(二元运算符)
|| 逻辑或(二元运算符)

其实 && 就是 数学中的 且 ,|| 就是数学的的 或

logical_op

逻辑运算符产生的结果仍然是 0 或 1,操作数经常也是 0 或 1,但这不是必需的。逻辑运算符将任何 非0值操作数当作真来处理,任何0值操作数当作假来处理。

💥 ❗️ “短路”计算

&& 和 || 运算符都遵循“短路”原则。

  1. 逻辑或短路(||):只要一个为真,或结果就为真,后面的不算了。
  2. 逻辑与短路(&&):只要有一个为假,结果就为假。如果&&左边的算式结果为假,那么右边的不再运行,直接得出结果为假。

如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
	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 语句

1. if

if 语句允许程序通过测试表达式的值从两种选项中选择一种。if 语句的简单格式如下:

1
2
3
if(表达式){
语句
}

如果语句部分只有一条语句,也可以写成

1
2
if(表达式)
语句;

执行 if 语句时,先计算圆括号内表达式的值。如果表达式的值非零(C语言将非零值解释为真值),那么接着执行大括号内的语句。例如:

1
2
if(i > 0)
printf("正数\n");

为了判定 k < i < j,可以这样写:

1
if(i > k && i < j)

为了判定相反的情况,可以写成:

1
if(i <= k || i >= j)

例2-1: 程序:为了判定一个数是不是大于零的,如果是,我们就输出提示语,然后让这个数加 1

1
2
3
4
if(i > 0){
printf("是正数\n");
i++;
}

2. else 子句

1
2
3
4
if(表达式)
语句;
else
语句;

如果是复合语句(compound statement),需要加上花括号

加上花括号是一种好习惯。建议不管是不是复合语句,尽量都加上花括号。

例2-2:增加需求:如果这个数不是正数,那么输出提示语,然后让这个数减 1

1
2
3
4
5
6
7
if(i > 0){
printf("是正数\n");
i++;
}else{
printf("不是正数\n");
i--;
}

3. 嵌套的 if 语句

例2-3:找出 i,j,k 中的最大值,并将其保存到 max 中

1
2
3
4
5
6
7
8
9
10
11
12
13
if(i > j){
if(i > k){
max = i;
}else{
max = k;
}
}else{
if(j > k){
max = j;
}else{
max = k;
}
}

4. 级联式 if 语句

编程时常常需要判定一系列的条件,一旦其中某个条件为真就立刻停止。

如何做到呢?

例2-4 程序:判断 n 是大于 0 还是 等于 0 还是小于 0

使用 if else

1
2
3
4
5
6
7
8
9
10
if(n < 0){
printf("n < 0");
}else{
if(n == 0){
printf("n = 0");
}
else{
printf("n > 0");
}
}

使用 else if

1
2
3
4
5
6
7
8
9
if(n < 0){
printf("n < 0");
}
else if(n == 0){
printf("n == 0");
}
else{
printf("n > 0");
}

这样写可以避免 if else 嵌套,从而提高了书写和理解的难易度。

级联式 if 语句书写形式:

1
2
3
4
5
6
7
8
9
if(表达式){
语句;
}
else(表达式){
语句;
}
else{
语句;
}

5. “悬空 else”问题

请看下面的程序,思考 else 与 那个 if 匹配

1
2
3
4
5
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 语句。为了避免这种问题,最好的办法就是加括号

1
2
3
4
5
6
if(y != 0){
if(x != 0)
printf("%.2f", x / y);
}
else
printf("Error: y is zero!");

6. 条件表达式

条件运算符(conditional operator): C 语言运算符中唯一一个三元(ternary)(三个操作数)运算符。

格式:

1
[条件表达式]表达式 1 ? 表达式2 :表达式3 ;

例2-6

1
2
3
4
5
6
if(x > 0){
x++;
}
else{
x--;
}

上面的程序我们用条件运算符可以这么写:

1
x > 0 ? x++ : x--;

判断 k 的值

1
2
3
4
i = 1;
j = 2;
k = i > j ? i : j; // k is 2 now
k = (i >= 0 ? i : 0) + j; // k is now 3

条件运算符使程序更短小但也更难以阅读,所以最好避免使用。然而有的情况会常常使用条件表达式。比如:

1)判断返回:

1
return i > j ? i : j;

2)printf

1
printf("%d\n", i > j ? i : j);

条件表达式也普遍应用于某些类型的宏定义中。

7. 布尔值(自行了解)

C89

多年以来,C语言一直缺乏适当的布尔类型。

一种解决方法是,先声明一个 int 型变量,让后将其赋值为 0 或 1:

1
2
3
4
int flag;
flag = 0; // 表示 flag 为 false
...
flag = 1; // 表示 flag 为 true

虽然这种方法可行,但是对于程序的可读性没有多大贡献,因为没有明确的表示 flag 的赋值只能是布尔型,并没有明确指出 0 和 1就是表示真和假。

为了使得程序更加便于理解,C89 程序员通常使用 TRUE 和 FALSE 这样的名字定义宏:

1
2
#define TRUE 1
#define FALSE 0

现在对 flag 的赋值就有了更加自然的形式:

1
2
3
flag = FALSE;
...
flag = TRUE;

为了判定 flag 是否为真,可以用:

1
2
3
if(flag == TRUE){
...
}

或者只写:

1
2
3
if(flag){
...
}

为了发扬这一思想,我们可以进一步定义一个用作布尔类型的宏:

1
#define BOOL int;

声明布尔类型变量时可以用 BOOL 替代 int

1
BOOL flag;

现在我们就非常清楚了:flag 不是一个普通的整型变量,而是表示布尔条件。(当然编译器还是将 flag 当作 int 类型的变量。)

C99

C99 中提供了 _Bool 类型:

1
_Bool flag;

_Bool是无符号整型。但是和一般的整形不同,_Bool 只能被赋值为 0 或 1 。一般来说,向 _Bool 类型变量中储存非零值会导致变量赋值为 1 。

1
2
3
4
5
_Bool flag = 5;
printf("%u", flag);

// 输出:
1

C99 还提供了一个新的头文件<stdbool.h>,改头提供了 bool 宏,用来代表 _Bool ;还提供了 true 和 false 两个宏,分别代表 1 和 0 。于是可以写:

1
2
3
4
5
6
#include<stdbool.h>

bool flag;
flag = true;
...
flag = false;

四 switch 语句

日常的编程中,常常需要把表达式和一系列值进行比较,从而找出当前匹配的值。

使用级联式 if 语句可以达到这个目的:

1
2
3
4
5
6
7
8
9
10
11
12
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 语句改写上面的程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 语句常用格式:

1
2
3
4
5
6
switch(控制表达式){
case 常量表达式 : 语句
...
case 常量表达式 : 语句
default : 语句
}
  • 控制表达式: 控制表达式只能用:整型,字符型的变量(C 语言把字符当成整数来处理),不能用浮点数 和 字符串。

  • 分支标号: 每一个分支的开头都有一个冒号,格式如下:

    case 常量表达式:

    常量表达式(constant expression): 必须是整数或字符型,不能包含变量和函数调用

    5 是常量表达式,5 + 10 也是常量表达式;但是 10 + n 不是常量表达式(除非 n 是表示常量的宏)。

  • 语句: 每个分支标号后可以跟任意数量的语句。不需要用花括号把这些语句括起来。每组语句的最后一条通常是 break 语句。

  • break 的作用: 本节后面会详细讨论。

  • default 语句的作用: 控制表达式的值和所有的标号语句都不匹配的话,会执行 default 后面的语句。(default :默认的意思)

C 语言不允许有重复的分支标号,但对分支的顺序没有要求,特别是 default 分支不一定要放在最后。

case 后只可以跟随一个常量表达式。但是,多个分支标号可以放置在同一组语句前面 。如:

1
2
3
4
5
6
7
8
9
10
11
switch(grade){
case 4:
case 3:
case 2:
case 1: printf("Passing");
break;
case 0: printf("Failing");
break;
default:printf("Illegal grade");
break;
}

为了节省空间,可以将拥有相同语句的分支标号放在同一行:

1
2
3
4
5
6
7
8
9
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 语句的作用

break 会使程序“跳出” switch 语句,继续执行 switch 后面的语句。

对控制表达式求值的时,控制会跳转到与 switch 表达式相匹配的分支标号处。分支标号只是说明 switch 内部位置的标记。在执行完分支的最后一句后,程序控制“向下跳转”到下一个分支的第一条语句上,而忽略下一个分支的分支标号。如果没有 break 语句(或者其他某种跳转语句),控制将会从一个分支继续到下一个分支。思考下面的 switch 语句:

1
2
3
4
5
6
7
8
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,那么显示的信息是:

1
GoodAveragePoorFailingIllegal grade

注意: 忘记 break 是编程时常见的错误。虽然有时候会故意忽略 break 以便多个分支共享代码,但是通常情况下省略 break 是因为忽略。

如果有需要,明确指出故意省略 break 语句是一个好主意:

1
2
3
4
5
6
7
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语言程序设计:现代方法》

  1. 写错误的程序比理解正确的程序简单。Epigrams on Programming 编程警句

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!