第三章

控制流

这一章主要讲控制流,一般来讲控制流语句就是用于控制各计算操作执行的次序

在之前的例子中使用了大量的控制流结构,本章将详细的介绍控制流语句

语句与程序块

本节问题

  1. 什么是语句,什么是程序块
  2. 如何使用这些语句和程序块,使用的时候要注意什么

语句

什么是语句,简单来讲就是在一个表达式后面加上分号;就变成了语句

例如:

1
2
x = 0;
i++;

这些表达式的后面加上了分号,代表这是一个语句

程序块

程序块指的是多个语句一起构成的复合语句,在语法上等价于单条语句

具体的用法是用花括号将这些语句括起来:

1
2
3
4
if (a > b){
x = a + b;
y = a * b;
}

需要特别注意的一点是,如果不加花括号,那么程序只会识别最近的一条语句

这一点于Python不一样,C语言更加严格,所以为了避免这样的事情发生,建议每一条语句都使用花括号,即使只有一条语句

并且与语句不同,用于结束程序块的右花括号后面并不需要加分号

if-else语句

本节问题

  1. 什么是if-else语句,特征是什么
  2. 在使用的时候有什么需要注意的点

if-else语句常用于判断,核心功能为非黑即白,也就是说,如果if条件是错的,那么一定执行else语句(这里考虑的是if-else语句,不涉及else-if语句)

接下来通过例子来说明

1
2
3
4
5
6
if (a>0){
b = 1;
}
else{
b = 0;
}

接下来解释这个例子各个条件的触发方式,首先最简单的一点,当a大于0的时候,则b的值为1,反之,也就是小于等于0的时候,b的值为0

需要注意的一点是,如果你只是想简单判断,可以不用加else,说白了就是else可以省略

由于if语句只是简单测试表达式的数值,所以一些写法可以稍微的简化

1
if (a != 0)

由于判断就只有两个结果,要不为真(1),要不为假(0)

所以这里 != 0相当于等于1

也就是可以这么写

1
if (a)

这种写法会相对比较简洁,但有些时候会导致表达模糊不清

配对else

在之前也说过,else语句是可以省略的,这就导致了在嵌套语句中一些省略else语句if语句会出现一些匪夷所思的歧义

解决方法也很简单,只需要加上花括号即可,这也就是为什么之前说即使只有一个语句也要养成加花括号的习惯

1
2
3
4
5
if (n > 0)
if (a > b)
z = a:
else
z = b;

加上花括号是这样的格式:

1
2
3
4
5
6
if (n > 0){
if (a > b)
z = a;
}
else
z = b;

这里可以发现,原来else语句是最外层的if语句的,而不是里面的if语句,这也就是为什么说不加花括号容易引起歧义

那么具体的规则是什么呢?C语言的默认规则:else语句始终与“最近的、未配对的if语句”绑定

什么叫未配对?if-else语句是一块的,只有if语句就是未配对

else-if语句

接下来讲讲else-if语句,这个语句的基本作用为弥补上面if-else语句的缺点——非黑即白

else-if语句可以进行多路判断,简单来说就是由之前的不是零就是一变成了可以有多个选择

具体的语法如下:

1
2
3
4
5
6
7
8
9
10
11
12
if (判断1){
语句1;
}
else if(判断2){
语句2;
}
else if(判断3){
语句3;
}
else{
语句4
}

与上面的if-else语句一样,这里末尾的else一样可以省略

接下来举个例子,我们想要判断你考试的分数是位于哪个等级

那么便可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int check(const int x) {
char grade;

if (x > 90) {
grade = 'A';
}
else if (x > 80) {
grade = 'B';
}
else if (x > 70) {
grade = 'C';
}
else {
grade = 'D';
}
return grade;
}

接下来模拟一下输入,首先假设输入的x为90,首先进入第一个判断(x > 90),发现不符合结果,所以跳转到第二个判断(x > 80)发现结果符合,所以等级就为B

由于else-if语句是整块的,只要有一个判断满足便会直接退出判断,不会继续判断下去


接下来通过十分经典的例题来说明

题目为:用二分法求数字

二分法这里不多概述,接下来讲讲思路

首先要找到目标那个数,所以要逐步缩小区间,由于不知道到底要缩小多少次区间,所以这里得用到循环

接下来是主体部分,首先第一个就是要判断中间那个数是否大于/小于目标数,这是第一个判断

接下来第二个判断,既然第一个判断是大于/小于,那么第二个就是与之相反的

最后这两种情况都判断了,剩下的也就是等于的情况了,所以直接留给else

那么这时候可以思考一下循环的条件,由于首先最基本的肯定是右边边界要大于左边边界

要不要等于呢?看看循环内的条件,如果没有等于的话,最后的else永远都不会触发,所以这里条件要有等于

接下来就是判断条件里面的内容了

首先是第一个判断条件,这里假设为中间的数大于目标值

这里直接想可能有点抽象,不妨让我们举个实际例子:

目标值:20,最低为0,最高为100,那么中间的数就是50

接下来可以看到,我们中间的数50大于目标值20

那么请问怎么样才可以让这个中间的值逼近目标值呢

既然中间值比目标值大,如果移动左边的最小值,那么中间值就会变大

移动右边的最大值,那么中间值就会变小,因此这里要选移动最大值

要移动多少合适呢由于中间值大于目标值,也就是说中间值到最大值这段范围都大于目标值

所以这里要选择中间值吗?我们想想看,如果相等,那么就直接结束了,所以这里选的数为中间值 - 1

第二个也是同理

最后代码如下:

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
27
int dichotomy(int x);

int main(void) {
printf("%d",dichotomy(7));
}

int dichotomy(int x) {

int high = 100,low = 0;
int count = 0;

while (high >= low) {
int mid = low + (high - low) / 2;

++count;
if (mid > x) {
high = mid - 1;
}
else if (mid < x) {
low = mid + 1;
}
else {
return count;
}
}
return -1;
}

这里可能就有疑问了,最后面的-1是什么,这里我们观察一下,实现这个-1的情况是什么?

对,就是那个没有被覆盖的情况:high < low

如果你试着用断点调试,你会发现,到后面由于目标值一直大于中位数,所以左边界会一直加上去,直到不满足条件

这样还是太费时间了,有没有更好的方法呢?

有的,只需要在一开始就判断目标数是否符合条件皆可:

1
2
3
if (x > high || x < low) {
return -1;
}

这样的话,就可以稍微优化一下

这里再次补充一点,为什么把这个判断写到这个循环里面,也就是:(high >= low && x <= high && x >= low)

这样看虽然没有了单独的if语句,但是由于每次进入循环都得判断,相较于原来的版本,每次要多2次比较,所以实际上是不太好的,这种优化称为悲观优化

补充内容:中间数计算

在上面的代码中可能会发现一个奇怪的点,就是在计算中间数的表达式似乎不太常规

也就是这里:

1
int mid = low + (high - low) / 2;

正常写是这样的:

1
int mid = (low + high) / 2;

这里可能就有人要说:“啊,这样写不就是为了炫技吗,在笔记写炫技内容合适吗?”

这里还真不是炫技,不妨我们假设一个情况,两者的数值都无限逼近int的最大值,此时正常写的版本会怎么样?

答案是数值溢出

而改进的版本很好的解决了这个问题

补充内容:语法糖

本节介绍的else-if语句实际上是一个语法糖

什么是语法糖,指计算机语言中添加的某种语法,这种语法对语言的功能没有影响,但是更方便程序员使用。它让程序更简洁,更清晰,更符合人的直觉。

原先的话长这样:

1
2
3
4
5
6
7
8
9
if (){

}
else{
if(){

}
else
}

这样的话如果数量一多,那么将会十分难以阅读

所以这也是为什么else-if语句是语法糖的原因

switch语句

接下来讲一下switch语句switch语句的作用是实现多路判定

这个表达式可以测试是否与一些常量值的某一个值匹配,如果匹配的话就执行相应的分支动作

这一点如果仔细观察的话就很像之前的if语句

具体的结构如下:

1
2
3
4
5
switch(判断内容){
case 常量表达式: 语句序列
case 常量表达式: 语句序列
default: 语句序列
}

每一个分支都可以用多个常量来标记,如果都没有的话就执行default语句

与之前的if语句一样,这里的default也是可以省略的(等价于else

与if语句不同,switch语句case的顺序是部分先后的,因为是查找是否一一对应

如果需要在执行后立刻退出判断,可以使用break语句直接退出判断

接下来我们用一个例子来说明这个用法

在之前我们有写过一个统计输入的程序,当时是选择使用if…else的结构来实现,这次我们用switch语句实现

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
int main() {
int i, c ,nums_white = 0, nums_other = 0,nums_digit[10];
for (i = 0;i < 10;i++) {
// 初始化计数器
nums_digit[i] = 0;
}
while ((c = getchar()) != EOF) {
switch (c) {
case '1': case '2': case '3': case '4': case '5':
case '6': case '7': case '8': case '9': case '0':
++nums_digit[c - '0'];
break;
case ' ': case '\n' : case '\t':
++nums_white;
break;
default:
++nums_other;
break;
}
}
for (i = 0; i < 10;++i)
printf("%d",nums_digit[i]);
printf("white = %d,other = %d",nums_white,nums_other);
return 0;
}

这里直接看到switch语句这里,可以列出了0~9的所有情况作为覆盖,只要有一个情况对得上就自动执行语句

其他的也是相同的道理

while循环与for循环

接下讲讲这两种循环,这两种循环在之前的总多实例中已经使用过很多次了,这里将详细地介绍这两种循坏语句

while循环

首先先讲讲while循环

其基本结构如下

1
2
while (表达式) 
语句

这里如果表达式的结果为真(1),那么则执行循环体内的语句

如果为假(0),则不执行循环

for循环

接下来讲讲for循环

其基本的结构有在第一章的笔记中提及到,这里再次声明一下

首先最基本的语句是:

1
2
3
for(表达式1;表达式2;表达式3){
循环语句
}

这里的第一个表达式为起始的表达式,一般执行循环一开始的时候会先执行这个表达式,之后在进入循环体

接下来是第二个表达式,也叫做判断表达式,其基本的功能为判断条件是否满足,如果满足的话则执行循环,否则不执行循环

最后一个为循环结束时的操作,也就是当一个循环结束的时候会额外干什么

需要注意的是,每一个语句都可以省略,但特别需要注意的一点是,如果第二个用于判断是否循环的表达式省略了,那么循环会一只执行下去

那么怎么避免这个问题呢?其实很简单,只需要在使用的时候在循环体中加入break或者return强制结束循环即可

两者的相似之处

接下来讲讲这两者有什么相似的地方

你可以将for循环拆解成这样

1
2
3
4
表达式1
while(表达式2)
语句
表达式3

但需要注意的一点是,并不是任何情况这两者都是相同的,如果循环中出现了continue语句,那么结果会变得不太一样

[TODO] Shellsort算法

逗号运算符

接下来讲讲逗号运算符,逗号运算符是C语言中优先级最低的一个运算符,一般在for语句会用到它,被这个运算符分割的一对表达式会按从左到右的顺序进行求值,例如:

1
for (i = 1,j = i + 2;;)

在上面这个for循环中,会先执行第一个语句,也就是i = 1,之后才会执行下一个表达式j = i + 2

利用这个功能,我们可以来实现书里面关于翻转字符的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void reverse(char s[]);

int main() {
char s[] = "Hello world!";
reverse(s);
printf("%s",s);
}

void reverse(char s[]) {
int i, j;
char temp;

for (i = 0,j = strlen(s) - 1; i < j ;i++,j--) {
temp = s[i];
s[i] = s[j];
s[j] = temp;
}
}

接下来开始说明一下思路

首先第一个点,我们需要知道我们要如何做,核心思想就是把两个字符的顺序反转,那么我们这里可以使用一个临时变量temp来存储我们想要交换的数字,然后让一开始的数字替换掉末尾的数字,之后把临时变量的值赋给没有交换的值(也就是开始的值)

举个例子:

第一位为:H,最后一位为:!

那么我们可以先用一个临时变量来储存!,之后将第一位的数字赋值给最后一位,这样就有了两个H

但是这时第一个H并没有交换,所以用临时变量(也就是刚才储存的最后一位)赋值给第一位

这样,第一位就变成了!,而最后一位就变成了H

最后代码实现如下:

1
2
3
4
5
6
7
8
9
10
void reverse(char s[]) {
int i, j;
char temp;

for (i = 0,j = strlen(s) - 1; i < j ;i++,j--) {
temp = s[i];
s[i] = s[j];
s[j] = temp;
}
}

可以看到,这里先初始化i,之后再计算j的值,strlen()的作用是获得字符串的长度(str + len)

使用需要引用这个头文件:string.h

do-while 循环

接下来讲讲do-while循环,这个循环使用的次数比较少,但在一些特定的场合显得十分有用

首先需要明白一件事情,这个循环与我们之前遇到的循环有什么区别?

像是之前的循环执行逻辑是这样的

1
判断是否满足 -> 满足则执行循环体内的语句

而这个循环是这样的

1
先执行语句 -> 判断是否满足 -> 满足则继续执行循环,否则则退出循环

具体的结构如下:

1
2
3
do
语句
while(表达式);

接下来通过一个例子来说明一下实际的用途:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void itoa(int n, char s[]) {
int i,sign;

if ((sign = n) < 0) {
n = -n;
}
i = 0;
do {
s[i++] = n % 10 + '0';
} while ((n /= 10) > 0);
if (sign < 0) {
s[i++] = '-';
}
s[i] = '\0';
reversed(s);
}

这里直接看到重点部分,也就是do-while语句里面的内容

首先,这个循环只有一个部分,也就是s[i++] = n % 10 + '0';

这个语句的作用是提取这个数的最后一位数字,而在提取之后便进入了判断的环节,在这个环节原先的数字会失去最后一位(n = 10)

如果还有数字则继续提取,反之则不提取

由于是从最后一位开始提取的原因,这里的数字的顺序是反着的,所以也就是为什么需要将整个字符串反过来的原因

break语句与continue语句

接下来讲讲break语句和continue语句,这两个语句共同的特点是当这两个语句位于循环中时,遇到这两个语句可以提前结束循环或开始新一轮的循环

break语句

接下来讲讲break语句

首先break语句的基本功能为结束当前的循环,举个例子:

1
2
3
4
5
6
7
8
9
int trim(char s[]){
int n;

for (n = strlen(s) - 1;n >= 0;n--)
if (s[n] != ' '&& s[n] != '\t'&& s[n] != '\n')
break;
s[n+1] = '\0';
return n;
}

在上面这里例子中,for循环内部使用了break语句作为循环的中断

这个语句的功能为,删去字符串中末尾的空格

那么是如何实现的呢,这就要讲到一个老生常谈的东西了:字符串以\0结尾

这里检测的是最靠近末尾的一个非空格,非制表符,非换行符的字符,所以只要找到这个字符,之后在它的下一位加上结束符\0,便可以实现截断末尾的功能

continue语句

接下来讲讲continue语句

continue语句的功能是跳过这个循环直接运行下一个循环

具体的功能不多赘述,接下来讲讲一些比较关键的点:如果while循环与for循环中出现了continue语句,那么这两者的作用则实际运行结果则不相同

首先先讲一下for循环的情况

如果在for循环中出现了continue语句,那么会先执行递增循环变量的这个不分(也就是第三个表达式)

在执行之后,才会继续进行判断和循环

举个例子来说明:

1
2
3
for(i = 0;i<1;++i){
continue;
}

如果执行这个循环会怎么样呢

首先看循环体内部只有一个continue语句,也就是说这个循环在执行完循环体后会直接执行递增循环变量++i

此时i的值就变成了2,而2是不满足循环条件的,所以就导致了循环不会继续进行,也就直接跳出循环

while循环又是怎样呢?

依旧通过一个例子来说明这个语句的实际效果

1
2
3
4
5
int i = 0;
while(i < 1){
continue;
++i
}

那么执行这个循环会直接进行下一次判断,而不会继续执行之后的++i

由于判断条件为i < 1,这就导致了无法退出循环,即一直在循环的问题

这也就是为什么说这两者有区别的原因

goto语句与标号

接下来讲讲goto语句与标号

首先需要介绍什么是goto语句

简单来讲,goto语句就是用于快速跳转到指定标号位置的语句

例如:

1
goto(error);

这个语句会跳转到标号为error的语句中

那么如何定义一个标号呢?只需要在标号后面加上冒号即可,例如:

1
error:

当然,在大多情况下使用goto语句的程序要比不使用goto语句的程序更加难以理解,一般来讲要尽量少使用goto语句

补充一点,任何使用goto语句的程序都可以改写为不带goto语句的程序