第三章
控制流
这一章主要讲控制流,一般来讲控制流语句就是用于控制各计算操作执行的次序
在之前的例子中使用了大量的控制流结构,本章将详细的介绍控制流语句
语句与程序块
本节问题
- 什么是语句,什么是程序块
- 如何使用这些语句和程序块,使用的时候要注意什么
语句
什么是语句,简单来讲就是在一个表达式后面加上分号;就变成了语句
例如:
1 | x = 0; |
这些表达式的后面加上了分号,代表这是一个语句
程序块
程序块指的是多个语句一起构成的复合语句,在语法上等价于单条语句
具体的用法是用花括号将这些语句括起来:
1 | if (a > b){ |
需要特别注意的一点是,如果不加花括号,那么程序只会识别最近的一条语句
这一点于Python不一样,C语言更加严格,所以为了避免这样的事情发生,建议每一条语句都使用花括号,即使只有一条语句
并且与语句不同,用于结束程序块的右花括号后面并不需要加分号
if-else语句
本节问题
- 什么是if-else语句,特征是什么
- 在使用的时候有什么需要注意的点
if-else语句常用于判断,核心功能为非黑即白,也就是说,如果if条件是错的,那么一定执行else语句(这里考虑的是if-else语句,不涉及else-if语句)
接下来通过例子来说明
1 | if (a>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 | if (n > 0) |
加上花括号是这样的格式:
1 | if (n > 0){ |
这里可以发现,原来else语句是最外层的if语句的,而不是里面的if语句,这也就是为什么说不加花括号容易引起歧义
那么具体的规则是什么呢?C语言的默认规则:else语句始终与“最近的、未配对的if语句”绑定
什么叫未配对?if-else语句是一块的,只有if语句就是未配对
else-if语句
接下来讲讲else-if语句,这个语句的基本作用为弥补上面if-else语句的缺点——非黑即白
else-if语句可以进行多路判断,简单来说就是由之前的不是零就是一变成了可以有多个选择
具体的语法如下:
1 | if (判断1){ |
与上面的if-else语句一样,这里末尾的else一样可以省略
接下来举个例子,我们想要判断你考试的分数是位于哪个等级
那么便可以这样写:
1 | int check(const int x) { |
接下来模拟一下输入,首先假设输入的x为90,首先进入第一个判断(x > 90),发现不符合结果,所以跳转到第二个判断(x > 80)发现结果符合,所以等级就为B
由于else-if语句是整块的,只要有一个判断满足便会直接退出判断,不会继续判断下去
接下来通过十分经典的例题来说明
题目为:用二分法求数字
二分法这里不多概述,接下来讲讲思路
首先要找到目标那个数,所以要逐步缩小区间,由于不知道到底要缩小多少次区间,所以这里得用到循环
接下来是主体部分,首先第一个就是要判断中间那个数是否大于/小于目标数,这是第一个判断
接下来第二个判断,既然第一个判断是大于/小于,那么第二个就是与之相反的
最后这两种情况都判断了,剩下的也就是等于的情况了,所以直接留给else
那么这时候可以思考一下循环的条件,由于首先最基本的肯定是右边边界要大于左边边界
要不要等于呢?看看循环内的条件,如果没有等于的话,最后的else永远都不会触发,所以这里条件要有等于
接下来就是判断条件里面的内容了
首先是第一个判断条件,这里假设为中间的数大于目标值
这里直接想可能有点抽象,不妨让我们举个实际例子:
目标值:20,最低为0,最高为100,那么中间的数就是50
接下来可以看到,我们中间的数50大于目标值20
那么请问怎么样才可以让这个中间的值逼近目标值呢
既然中间值比目标值大,如果移动左边的最小值,那么中间值就会变大
移动右边的最大值,那么中间值就会变小,因此这里要选移动最大值
要移动多少合适呢由于中间值大于目标值,也就是说中间值到最大值这段范围都大于目标值
所以这里要选择中间值吗?我们想想看,如果相等,那么就直接结束了,所以这里选的数为中间值 - 1
第二个也是同理
最后代码如下:
1 | int dichotomy(int x); |
这里可能就有疑问了,最后面的-1是什么,这里我们观察一下,实现这个-1的情况是什么?
对,就是那个没有被覆盖的情况:high < low
如果你试着用断点调试,你会发现,到后面由于目标值一直大于中位数,所以左边界会一直加上去,直到不满足条件
这样还是太费时间了,有没有更好的方法呢?
有的,只需要在一开始就判断目标数是否符合条件皆可:
1 | if (x > high || x < low) { |
这样的话,就可以稍微优化一下
这里再次补充一点,为什么把这个判断写到这个循环里面,也就是:(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 | if (){ |
这样的话如果数量一多,那么将会十分难以阅读
所以这也是为什么else-if语句是语法糖的原因
switch语句
接下来讲一下switch语句,switch语句的作用是实现多路判定
这个表达式可以测试是否与一些常量值的某一个值匹配,如果匹配的话就执行相应的分支动作
这一点如果仔细观察的话就很像之前的if语句
具体的结构如下:
1 | switch(判断内容){ |
每一个分支都可以用多个常量来标记,如果都没有的话就执行default语句
与之前的if语句一样,这里的default也是可以省略的(等价于else)
与if语句不同,switch语句中case的顺序是部分先后的,因为是查找是否一一对应
如果需要在执行后立刻退出判断,可以使用break语句直接退出判断
接下来我们用一个例子来说明这个用法
在之前我们有写过一个统计输入的程序,当时是选择使用if…else的结构来实现,这次我们用switch语句实现
1 | int main() { |
这里直接看到switch语句这里,可以列出了0~9的所有情况作为覆盖,只要有一个情况对得上就自动执行语句
其他的也是相同的道理
while循环与for循环
接下讲讲这两种循环,这两种循环在之前的总多实例中已经使用过很多次了,这里将详细地介绍这两种循坏语句
while循环
首先先讲讲while循环
其基本结构如下
1 | while (表达式) |
这里如果表达式的结果为真(1),那么则执行循环体内的语句
如果为假(0),则不执行循环
for循环
接下来讲讲for循环
其基本的结构有在第一章的笔记中提及到,这里再次声明一下
首先最基本的语句是:
1 | for(表达式1;表达式2;表达式3){ |
这里的第一个表达式为起始的表达式,一般执行循环一开始的时候会先执行这个表达式,之后在进入循环体
接下来是第二个表达式,也叫做判断表达式,其基本的功能为判断条件是否满足,如果满足的话则执行循环,否则不执行循环
最后一个为循环结束时的操作,也就是当一个循环结束的时候会额外干什么
需要注意的是,每一个语句都可以省略,但特别需要注意的一点是,如果第二个用于判断是否循环的表达式省略了,那么循环会一只执行下去
那么怎么避免这个问题呢?其实很简单,只需要在使用的时候在循环体中加入break或者return强制结束循环即可
两者的相似之处
接下来讲讲这两者有什么相似的地方
你可以将for循环拆解成这样
1 | 表达式1 |
但需要注意的一点是,并不是任何情况这两者都是相同的,如果循环中出现了continue语句,那么结果会变得不太一样
[TODO] Shellsort算法
逗号运算符
接下来讲讲逗号运算符,逗号运算符是C语言中优先级最低的一个运算符,一般在for语句会用到它,被这个运算符分割的一对表达式会按从左到右的顺序进行求值,例如:
1 | for (i = 1,j = i + 2;;) |
在上面这个for循环中,会先执行第一个语句,也就是i = 1,之后才会执行下一个表达式j = i + 2
利用这个功能,我们可以来实现书里面关于翻转字符的例子
1 | void reverse(char s[]); |
接下来开始说明一下思路
首先第一个点,我们需要知道我们要如何做,核心思想就是把两个字符的顺序反转,那么我们这里可以使用一个临时变量temp来存储我们想要交换的数字,然后让一开始的数字替换掉末尾的数字,之后把临时变量的值赋给没有交换的值(也就是开始的值)
举个例子:
第一位为:H,最后一位为:!
那么我们可以先用一个临时变量来储存!,之后将第一位的数字赋值给最后一位,这样就有了两个H
但是这时第一个H并没有交换,所以用临时变量(也就是刚才储存的最后一位)赋值给第一位
这样,第一位就变成了!,而最后一位就变成了H
最后代码实现如下:
1 | void reverse(char s[]) { |
可以看到,这里先初始化i,之后再计算j的值,strlen()的作用是获得字符串的长度(str + len)
使用需要引用这个头文件:string.h
do-while 循环
接下来讲讲do-while循环,这个循环使用的次数比较少,但在一些特定的场合显得十分有用
首先需要明白一件事情,这个循环与我们之前遇到的循环有什么区别?
像是之前的循环执行逻辑是这样的
1 | 判断是否满足 -> 满足则执行循环体内的语句 |
而这个循环是这样的
1 | 先执行语句 -> 判断是否满足 -> 满足则继续执行循环,否则则退出循环 |
具体的结构如下:
1 | do |
接下来通过一个例子来说明一下实际的用途:
1 | void itoa(int n, char s[]) { |
这里直接看到重点部分,也就是do-while语句里面的内容
首先,这个循环只有一个部分,也就是s[i++] = n % 10 + '0';
这个语句的作用是提取这个数的最后一位数字,而在提取之后便进入了判断的环节,在这个环节原先的数字会失去最后一位(n = 10)
如果还有数字则继续提取,反之则不提取
由于是从最后一位开始提取的原因,这里的数字的顺序是反着的,所以也就是为什么需要将整个字符串反过来的原因
break语句与continue语句
接下来讲讲break语句和continue语句,这两个语句共同的特点是当这两个语句位于循环中时,遇到这两个语句可以提前结束循环或开始新一轮的循环
break语句
接下来讲讲break语句
首先break语句的基本功能为结束当前的循环,举个例子:
1 | int trim(char s[]){ |
在上面这里例子中,for循环内部使用了break语句作为循环的中断
这个语句的功能为,删去字符串中末尾的空格
那么是如何实现的呢,这就要讲到一个老生常谈的东西了:字符串以\0结尾
这里检测的是最靠近末尾的一个非空格,非制表符,非换行符的字符,所以只要找到这个字符,之后在它的下一位加上结束符\0,便可以实现截断末尾的功能
continue语句
接下来讲讲continue语句
continue语句的功能是跳过这个循环直接运行下一个循环
具体的功能不多赘述,接下来讲讲一些比较关键的点:如果while循环与for循环中出现了continue语句,那么这两者的作用则实际运行结果则不相同
首先先讲一下for循环的情况
如果在for循环中出现了continue语句,那么会先执行递增循环变量的这个不分(也就是第三个表达式)
在执行之后,才会继续进行判断和循环
举个例子来说明:
1 | for(i = 0;i<1;++i){ |
如果执行这个循环会怎么样呢
首先看循环体内部只有一个continue语句,也就是说这个循环在执行完循环体后会直接执行递增循环变量++i
此时i的值就变成了2,而2是不满足循环条件的,所以就导致了循环不会继续进行,也就直接跳出循环
而while循环又是怎样呢?
依旧通过一个例子来说明这个语句的实际效果
1 | int i = 0; |
那么执行这个循环会直接进行下一次判断,而不会继续执行之后的++i
由于判断条件为i < 1,这就导致了无法退出循环,即一直在循环的问题
这也就是为什么说这两者有区别的原因
goto语句与标号
接下来讲讲goto语句与标号
首先需要介绍什么是goto语句
简单来讲,goto语句就是用于快速跳转到指定标号位置的语句
例如:
1 | goto(error); |
这个语句会跳转到标号为error的语句中
那么如何定义一个标号呢?只需要在标号后面加上冒号即可,例如:
1 | error: |
当然,在大多情况下使用goto语句的程序要比不使用goto语句的程序更加难以理解,一般来讲要尽量少使用goto语句
补充一点,任何使用goto语句的程序都可以改写为不带goto语句的程序