我们已经知道了最基本的程序如何进行顺序执行,但是,几乎所有实际有用的程序都会有条件判断,甚至会根据不同的条件执行不同的代码.

那么如果程序中需要进行条件判断,进而选择不同的路径进行执行(即执行一部分代码,同时忽略另一部分代码),我们就需要语言支持选择流程控制.

我们来学习C语言是如何实现条件判断选择控制的.

前置知识

什么是真,什么是假

不像其他语言,C语言(包括C++)十分简单粗暴:0是假,只要不是0,那么就都是真

这句话隐含一个非常的事情—那就是C语言中所有的值都可以用于判断,也就是说,不像其他语言有类似bool这样专门的类型,C语言只要是任何一个表达式,都可以用于逻辑判断,并且,即使是逻辑表达式,其结果也只会使普通的1(真)或0(假).

那么问题来了:

1.默认情况下,0是假,1是真,那么2,3,4,5这样的数是真还是假呢?

​ 答案是!因为非0即为真,只要不是整数0,那么都是真值,同理,一个浮点数1.1也是.

2.最重要的一个问题,也是最容易忽略的一个问题,像-1,-2,-3这样的负数是真还是假呢?

​ 答案也是!因为非0即为真,负数怎么可能等于0呢?那不就是的嘛.

—WAHAHA,2023.9.22

逻辑运算符

很显然,逻辑运算符用于进行逻辑判断,其构成的逻辑表达式返回1(成立)或0(不成立)这两个整数中的某一个.

这里简单的介绍一下,非常简单.

逻辑运算符有3种—与或非,分别是&&,||!

1
2
3
4
5
`&&`就是`与`的意思,例如`a&&b`要求a和b必须都是真的,整个表达式才是真的

`||`就是`或`的意思,例如`a||b`要求a和b只要有一个是真的,整个表达式就是真的

!就是`非`的意思,他只有一个操作数,例如`!a`就是对a取反,如果`a`为真,那么`!a`就是`假`;如果`a`为假,那么`!a`就是`真`

以上的a和b都是表达式,无论复杂度多高,只要是一个合法的表达式即可

并且,虽然非0即为真,但是作为逻辑运算符,其结果一定是1或0

重点:短路效应

同时,&&||还有一个非常非常重要的特性,那就是短路

1.对于a&&b

短路的意思是,如果对表达式a求值,发现a已经是假的了,那么对b求值已经无所谓了,因为整个表达式势必是假,所以C编译器没有必要再费功夫去计算b,则直接返回假(0),这个现象叫做短路与

2.对于a||b

短路的意思是,如果对表达式a求值,发现a已经是真的了,那么对b求值已经无所谓了,因为整个表达式势必是真,所以C编译器没有必要再费功夫去计算b,则直接返回真(1),这个现象叫做短路或

短路效应应该很好理解,可以理解为,如果前一个表达式已经足以说明最终的结果了,那么就没有必要再去计算后面的表达式,从效果上看就好像后面的表达式被短路了一样.

注意事项

同时,必须注意的一点是:所谓的短路,指的是完全不进行求值,而不是求值后无效果!

之所以要说明这一点的原因是:任何表达式都可以作为逻辑运算符的运算对象,而并不是所有的表达式运算后都没有任何影响的.

例如,我们之前讲解的赋值运算符,如a=3,这将给a赋一个新值3,但是实际上,这个效应可以理解为对a=3这个表达式进行求值的副作用,因为a=3不仅仅只是给a赋值那么简单,他要返回一个值,而这个值就是=左边的a的值.

那么既然a=3也会返回一个值,那么他就有权利成为&&或者||的运算对象—3代表,如果这个表达式作为第二个表达式,如果他被计算,那么a就会被覆盖为3,但是如果他被短路了,那么a=3就根本不会被计算(求值),进而a仍然保留原值.

如果上述的结果并不是你想要的,那么这就是一个严重的逻辑错误,因为a的值很可能在判断的过程中就已经被破坏了!!!

这个问题十分重要,同时提前引入了表达式的副作用这个重要概念,相关的讲解将会在后续讲解运算符求值顺序等相关知识时进行详细的讲解!

关系运算符

比起逻辑运算符,关系运算符可能更容易一点,说简单点,就是比大小.

他们同样返回1或0.下图截取自菜鸟教程

image-20230924195636312

注意,==运算符需要两个=,而非一个=,这与一些语言并不相同.一个=在C语言中是赋值运算符.

什么是选择执行

大多数问题的求解中不可能只出现一种情况,甚至是在运算过程中必然出现需要选择的情况,此时,我们就需要根据当前的情况(也就是分支条件)选择一条适当的部分进行执行,而忽略另外的部分.

例如,我们的问题是输出2个数a,b中的最大值,这就面临一个最简单的选择:究竟是a大还是b大,显然,如果a大则输出a,如果b大则只会输出b. C语言的选择语句就是为了处理这种情况所设计.

if选择语句

所有的高级语言都有流程控制语句的实现,而且目前主流的几种语言的语法都是相似的.

C语言中,使用if...else if...else语句进行选择控制.需要提出的一点是,这里所说的语句和标准表述的不同,但是为了更好地理解,这里把他们整合为一个大的语句.

该语句的结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
if(表达式1){
//代码块1
}else if(表达式2){
//代码块2
}else if(表达式3){
//代码块3
}else if(...){
...
}else if(代码块n){
//代码块n
}else{
//代码块n+1
}

需要注意的一点是,上面说的各个表达式,实际上可以是任何表达式(返回值的),因为使用了关系运算符和逻辑运算符的条件,实际上也是表达式,因为他们返回1或0这两种逻辑值,所以用于逻辑判断是没有问题的.

一般情况下,我们确实仅仅将逻辑表达式放进去,但是后续我们会看到一些更"高级"/"抽象"的写法.

if语句对括号()中的表达式进行求值,对应到包含有关系运算符和逻辑运算符的表达式自然就是判断条件.

其中,可以看出else if(){}的数量从逻辑上并没有任何限制(但是编译器有最大限制,基本用不到那么多的分支),编译器允许我们的数量足够我们使用.else if(){}的用途是,如果我们在if()中的条件(表达式1)并不符合,我们可以继续判断其他的条件,依次判断,直到确定进入一个else if(){}中.

同理,else{}用于如果if(){}和所有的else if(){}都不满足条件,那么将会执行else中的代码块n+1.

另一方面,除了第一个if(){}是必须有且仅有一个,其他所有的else if(){}和必须放在最后的else{}都是可有可无的,他们是否存在取决于你的代码是否需要这么多平行的条件.

讲了这么多理论,理解力强的朋友可能已经懂了,但是为了清楚说明,还是要举几个例子.

示例

菜单选择

写一个程序,输入1,2,3中的一个数,分别输出"hello",“good”,“great”,如果输入的不是1,2,3中的任何数,则输出"sorry".

代码很简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
int main() {
int n;
scanf("%d",&n);
if(n==1){
printf("hello");
}else if(n==2){
printf("good");
}else if(n==3){
printf("great");
}else{
printf("sorry");
}
return 0;
}

读者可以尝试运行一下.其中,else{}即用于处理除了1,2,3以外任何输入的值!

成绩评级

输入一个0~100的整数作为成绩,

如果大于等于90,评为A;

如果大于等于80且小于90,评为B;

如果大于等于60且小于80,评为C;

如果小于60,评为D.

这里的判断要用到关系运算符(也就是比大小):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>

int main() {
int score;
scanf("%d", &score); // 用于输入
if (score >= 90 && score <= 100)
printf("A");
else if (score >= 80 && score < 90)
printf("B");
else if (score >= 60 && score < 80)
printf("C");
else if (score < 60 && score >= 0)
printf("D");
else
printf("错误,必须为0~100的成绩");
return 0;
}

讲解

成绩评级这个程序中,我们可以看到,我们使用了一个较为复杂的表达式,用于限定score变量的范围,我们使用了<=>=来界定,同时使用&&来连接这两个子表达式.

必须注意的是,&&优先级>=<=高,这意味着先判断<=>=,然后才会把两个判断的结果&&起来.

依旧,有关优先级的知识将会放在后面进行讲解.

另一方面,我们加入了score<=100score>=0的判断,用于处理错误输入—这意味着我们意识到运行该程序的人可能输入一个错误的值来测试我们的程序.

如果我们只关心问题的求解,而不去关心一些可能出现的异常情况,那么一旦出现一些误输入,甚至是故意而为之,我们的程序就会给出错误的结果,甚至崩溃.

这里的多一步判断,实际上加强了该程序的健壮性,也就是说,这个程序能一定程度上处理一些错误操作,而不会发生错误.

作为程序的编写者,不要指望使用程序的人(甚至是我们自己)有多么高明,他可能会不小心输入各种错误信息,或者会故意输入错误的信息,此时,就需要你去进行良好的处理.

switch语句

除了if语句,如果我们判断的只是一个表达式的值—整数,那么我们可以使用switch语句来简化.

switch的一般结构是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
switch(表达式){
case 常量1:
//代码段1
break;
case 常量2:
//代码段2
break;

... // 任意数量的case语句---同样不能超出编译器允许的最大数量

case 常量n:
//代码段n
break;
default:
//默认执行代码段
break;
}

解释

1.在上面的模板中,我们只对表达式进行一次求值,然后对这个值从上向下依次尝试匹配,如果一旦从一众常量中匹配到一个,那么就从该case进入,依次向下执行代码,直到遇到break;结束或者switch尾部结束.

2.必须注意的是,表达式求值后必须为整数,因为我们要对其进行精确匹配,而浮点数会有精度问题,很难保持绝对的相等.

3.非常重要的一点是:case仅仅是一个标号—C语言中的case是真正意义上的标号而已,一旦匹配到后,就不会对后面的语句有任何影响,于是,如果你的当前case没有break;,那么他会穿过下一个没有匹配到的case一直执行下去,直到在任何地方遇到一个break;或者switch尾部才结束—我们会举例强调这个重要特性!

4.default可有可无,而且其位置任意.这意味着两件事:首先,default类似else{},只要没有匹配到任何case标签,那么就会执行default;其次,default可以放在任意case之前/之后,对其没有任何影响,但是作为规范,我们一般都将其放在最后!

上面的解释实际已经非常全面了,如果一下子没有完全理解,不要紧,我们上例子:

示例

模拟菜单

我们模拟一个选择菜单,输入1,2,3,4分别输出"开始游戏",“查看排名”,“设置”,“退出”,如果没有匹配到就输出"输入错误".

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>

int main() {
int score;
scanf("%d", &score); // 用于输入
switch (score) {
case 1:
printf("开始游戏");
break;
case 2:
printf("查看排名");
break;
case 3:
printf("设置");
break;
case 4:
printf("退出");
break;
default:
printf("输入错误");
}
return 0;
}

就是这么简单,记得每个printf后面都要加上break;,因为default在最后面,所以可以不加break.下面我们尝试一下如果不加break会发生什么:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>

int main() {
int score;
scanf("%d", &score); // 用于输入
switch (score) {
case 1:
printf("开始游戏");
case 2:
printf("查看排名");
case 3:
printf("设置");
case 4:
printf("退出");
default:
printf("输入错误");
}

return 0;
}

运行结果如图:

image-20230927012301497 image-20230927012319882 image-20230927012332979

显然,我们发现输出完对应的语句后并没有停止,而是向下一直运行—因为一直没有遇到break;语句.读者可以自行尝试在中间适当位置加上break;来测试是否会在此处停止.

另外,我们测试一下default的位置:

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>

int main() {
int score;
scanf("%d", &score); // 用于输入
switch (score) {
case 1:
printf("开始游戏");
break;
case 2:
printf("查看排名");
break;
default:
printf("输入错误");
break; // 要记得加上`break;`!
case 3:
printf("设置");
break;
case 4:
printf("退出");
break;
}

return 0;
}

运行结果:

image-20230927012606490

可以发现并没有任何的影响.但是要注意因为default不在末尾了,要记得加上break;!

最后,我们可以利用一下switch没遇到break;就不会停止(到达尾部除外)的特性,写一个程序:

及格判断

输入’A’,‘B’,‘C’,'D’这4个评级之一,判断是否及格—除了’D’都及格.同时,要判断错误输入.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <stdio.h>

int main() {
char level;
scanf("%c", &level); // 用于输入
switch (level) {
case 'A':
case 'B':
case 'C':
printf("恭喜你,及格了!");
break;
case 'D':
printf("很遗憾,不及格!");
break;
default:
printf("输入错误!");
break;
}

return 0;
}

代码中可以看出,无论是匹配到’A’,‘B’,'C’中的任何一个,都会执行10,11两行—因为前面的标签后面没有任何语句!包括break;也没有!所以一直向下到第10行,这样一定程度上简化了代码—但是绝对不能滥用,因为毕竟这是C语言的一个特性,要慎重!

运行结果如下:

image-20230927013445783 image-20230927013502980 image-20230927013514457 image-20230927013526700

至此,选择执行的语句基本介绍完毕,但是有些细节—例如break;语句究竟是什么?等问题,还有嵌套使用等都要在后面进行详细的讲解.

---WAHAHA 2023.9.27



上一篇:C语言教程-5-顺序执行

下一篇:C语言教程-7-循环执行