You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

288 lines
17 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 03 | 判断与循环:给你的程序加上处理逻辑
你好,我是胡光,咱们又见面了。不知道上一讲的内容,你自己做练习了么?你是否还觉得 C 语言枯燥无味呢?不管你有没有练习,我都还要啰嗦下,学习编程,就像是学骑自行车,你只看别人怎么骑,你只看自行车的原理,那永远也不可能学会骑自行车,对于你来说,唯一的捷径就是多练习,多思考。在上一讲小试牛刀之后,今天我将带你领略一下算法和逻辑层面的小惊喜。
## 今日任务
先来看看今天这 10 分钟我们要完成的任务。日期这个概念你肯定不陌生生日对你我来说都很重要如果你身边有2月29号过生日的小伙伴恐怕最少4年才能为他/她办一次生日宴。今天我们的这个任务,就和日期有关系。如果我给你一个由年月日组成的日期,再给你一个数字 X你能否准确地让程序输出 X 天后的日期呢?
例如下面这个数据:
```
1989 11 20
20
1989 12 10
```
数据中给出了1989年11月20日这个日期然后问你20天后的日期是多少你的程序应该输出1989年12月10日。特别需要注意的是在这个任务中你需要考虑到闰年中2月份的特殊性闰年的2月有29天。今天我们就学习如何用计算机解决这类任务吧。
## 必知必会,查缺补漏
根据对任务的理解,我们可以分成两步来思考这个问题:
* 第一步我们来思考如何求解1天后的日期在求解1天后日期的过程中我们涉及到的核心问题就是判断日子是否跨月月份是否跨年即判断
* 第二步:是要让计算机重复 X 次1天后日期的计算过程即重复循环做这件事。
要解决这两个难题,我们需要讲讲 C 语言中的一些基础知识,其中包括了程序中用于**逻辑分支判断的分支结构**,以及可以**重复做大量事情的循环结构**。听着这些专业词汇,你可能有点懵,别怕,等我下面讲了它们是什么意思,你就会感觉这些其实很简单。
#### 1.给代码添加判断能力“if…else”语法结构
我们一起来读下这句话:如果隔壁商店有酱油,就买酱油,否则就买点儿醋回来。可以看到,这句话用了“如果……就”的假设关系关联词,“如果”后面接的是假设条件,“就”后面接的是条件成立后的结果,“否则”接的是条件不成立后的结果。
现在我们想把计算机变成我们的小帮手,就必须要有一种语法,能够表达 “如果……就……否则……”的逻辑,这种语法,就是接下来我要介绍给你的 “if…else”语法结构。
在这里我将简单介绍“if…else”语法结构主要目的是让你看懂今天我们这个任务的代码后续在课程逐步展开的过程中我还会逐步的引入这个语法结构的一些其他知识点。
我们先来看 “if…else”最基本的语法结构
```
if (条件表达式) 一条语句1;
else 一条语句2;
```
简单来说if 和 else 都是关键字,代表分支逻辑中的 “如果”和 “否则”。if 后面跟着的括号里面,需要放一个条件表达式,条件表达式如果成立,程序会执行 “语句1”否则就会执行 “语句2”。下面我来举个例子你就明白了
```
#include <stdio.h>
int main() {
int a, b;
scanf("%d%d", &a, &b);
if (a == b) printf("a is equal to b!\n");
else printf("a is not equal to b!\n");
return 0;
}
```
这段程序中,首先定义了两个变量 a 和 b然后通过输入函数scanf给变量 a、b 赋值。之后就是重点部分了,根据我们上面所说的,如果 if 后面的条件表达式成立,那么就会输出 “a is equal to b!\\n” 否则就会输出 “a is not equal to b!\\n”。
最后,我就再带你理解两个概念,一是条件表达式是什么,二是怎样理解 if 后面跟一条语句,所谓一条语句的概念范围是什么。
回到上面的程序中,你会看到程序中的 if 后面跟着一个括号,括号里面放着一个表达式,这个就是我们所谓的条件表达式,而这个括号,是必不可少的。我们发现,这个条件表达式用**两个等号**连接 a 和 b作用是判断 a 和 b 里面存储的值是否相等。可千万别跟赋值表达式的**一个等号**弄混了。
说到这里我要告诉你一个重要的事实变量有变量对应的值表达式也有表达式对应的值。那么例如上面代码中的条件表达式“a == b”所对应的值是什么呢其实就是数字 1 或者 0分别表示“条件成立”a与b的值相等和“不成立”a与b的值不相等
> **延伸内容:**
> 那么除了条件判等以外还有哪些条件运算符呢有判断不等于的“a != b”大于的 “a > b”小于的 “a < b”大于等于的 “a >= b”小于等于的 “a <= b”逻辑非 “!(a > b)”,等价于 “a <= b”。同时多个条件表达式还可以用逻辑 && 和 || 进行连接,这个后面我再跟你细说。
事实上if 的括号里面,不仅可以放条件表达式,类似于 “a - b”这种的表达式也是可以当做 if 的条件的。
当一般表达式作为条件的时候if 是怎么执行的呢?很简单,记住:**表达式的值,非 0 即为真**。例如,下面两行代码,效果等价:
```
if (a != b) printf("a is not equal to b!\n");
if (a - b) printf("a is not equal to b!\n");
```
你会看到,第二行代码中,用 “a - b”代替 “a != b”取得了同样的程序运行效果。因此你只需要重点思考表达式 “a - b” 什么时候结果非 0 即可,是不是当且仅当 “a != b”时“a - b”的结果非 0根据之前所说的非 0 即为真,那么 if 条件也就算是成立了。
最后,我们来讲一下怎么理解“**if 后面跟一条语句**”这个概念,其实指的是 if后面的条件成立时所执行的代码。这里我们的重点是要理解一条语句都包含什么形式大致可以分为如下几类。
**第一种,空语句**,就是什么都没有,单纯以一个分号结尾,例如下面这行代码,即使条件成立,也不会有任何实质上的操作。
```
if (a == 3) ;
```
**第二种,单一语句**,比空语句多了语句内容,以分号结尾,例如下面这行代码,当条件成立的时候,会输出 “hello geek!”。
```
if (a == 3) printf("hello geek!\n");
```
**第三种,复合语句**,被大括号包裹,中间是若干条语句,例如下面这段代码:
```
if (a == 3) {
printf("hello geek1!\n");
printf("hello geek2!\n");
printf("hello geek3!\n");
}
```
当条件成立以后,程序会依次执行大括号里面的三条语句:
```
hello geek1!
hello geek2!
hello geek3!
```
**第四种,结构语句**,以 ifforwhile 等开头的分支语句或循环语句,例如下面这段代码,首先会先判断 a==3如果条件成立才会执行下面第二条 if 分支语句,当第二条 if 分支语句的条件也成立的时候,才会输出 “hello geek!”。
```
if (a == 3)
if (b == 4) {
printf("hello geek!\n");
}
```
由此可以看到if 后面所谓跟着的一条语句,还真是丰富多彩,你可以在后面跟上像上面代码中所写的 printf 函数调用的单一语句,也可以用一个大括号,里面放上若干条语句,亦或是 if 后面跟着另一个 if 也是可以的!你看这种组合能力,有没有点儿像乐高玩具?
至此,你就已经掌握了基础的将 “如果……就……否则……”这种逻辑结构转换成代码的能力了。你的计算机,终于有了“判断力”。
#### 2.给程序添加重复执行功能for和while语句
想想小的时候你最讨厌什么事情我最讨厌的就是被老师罚写汉字错一个字罚写100遍那种的在我看来真的是在浪费时间。可当我学了程序以后我发现程序真的是特别擅长做这种重复的工作而实现这种功能的语法结构就是 for 语句和 while 语句。
我们先来看语法结构较简单的 while 语句:
```
while (循环条件) 一条语句;
```
以 while 关键字开头后面跟着循环条件也就是一个条件表达式然后是一条语句。while 循环,顾名思义,当循环条件成立时,就会执行一次后面的语句,之后就是再判断循环语句是否成立,如果成立就再执行,一直到循环条件不成立为止。
下面呢,我们就用最简单的形式,利用 while 循环输出前100个正整数
```
int i = 0;
while (i++ < 100) printf("%d\n", i);
```
这段代码里面,出现了一个你之前没有见过的语法,就是 i++,这也是表达式,这个表达式的值等于 i 之前的值当这条表达式执行完以后i 会变成 i + 1 的值。例如起初i = 2i++ 表达式的值就等于 2可表达式执行以后你要是输出 i 的值,这时 i 实际等于 3。
上面代码中,我们是用 i++ 表达式的值和 100 进行比较,表达式的值会遍历 0 到 99所有的值由于 printf 在 i++之后输出 i 的值,所以实际上每次输出的都是 i + 1之后的值也就是说 printf 会输出 1100 所有值。具体的你可以参考下面的这个程序流程图。
![](https://static001.geekbang.org/resource/image/a3/f4/a37e05821040b86c2226ac60b95dacf4.jpg "while 循环流程图")
另外,顺便再问你个问题,你还记得上一节课里,我们学到的\\n和%d分别代表什么意思嘛如果不记得记得回去再复习下。
有了 while 循环语句的加持之后,是不是重复做某件事,变得很方便了呢?不急,下面我要给你介绍的是功能更为强大的 for 语句。还是先来看一下 for 语句的结构吧:
```
for (初始化①;循环条件②;循环后操作③) 一条语句④;
```
正如你看到的,我把 for 语句的四部分已经给你标出来了for 语句会按照 ①②④③②④③…循环,直到某一次循环条件②不成立了为止。
你会发现,①这一部分只在循环开始时执行了一次,真正所谓的循环,是以循环条件②,一条语句④以及循环后操作③组成的。
如果要是用 for 循环输出 1100 所有值,会显得代码更清晰一些:
```
for (int i = 1; i <= 100; i++) printf("%d\n", i);
```
上面这段代码,就是用 for 循环实现了和之前 while 循环相同的功能。
看了 for 循环和 while 循环以后你可能会问实际中哪种循环用的比较多我个人经验来说for 循环用的比较多,因为 for 循环每一部分都非常明确对于比较复杂的循环控制过程for 循环写出来以后,一般都会比 while 循环可读性强。
为了让你感受到 for 循环真正的威力,写一段代码,让你感受一下:
```
for (int i = 1, k = 0; i <= 48; i++, k += 2) printf("%d\n", k);
```
上面这段程序中我们用到了两个同步信息变量i 和 ki 从 1 到 48保证循环了48次代码中“k+=2”表示k每次增加 2 也就是说在这个过程中i 遍历了 1 到 48 这 48 个整型值,而 k 同步地遍历了从 0 开始的前 48 个偶数。这段代码的意思其实就是打印出从0开始后的共48个偶数即0、2、4……92、94。
如果用while来实现这个目的知道怎么写吗你可以自己在计算机上试一下。
## 一起动手,搞事情
#### 思考题:打印乘法表
> 使用循环和条件判断打印一个格式优美的66乘法表
> 要求1输出内容及样式参照下面给出的样例
> 要求2每两列之间用 \\t 字符进行分隔,行尾无多余 \\t 字符
```
1*1=1
1*2=2 2*2=4
1*3=3 2*3=6 3*3=9
1*4=4 2*4=8 3*4=12 4*4=16
1*5=5 2*5=10 3*5=15 4*5=20 5*5=25
1*6=6 2*6=12 3*6=18 4*6=24 5*6=30 6*6=36
```
## “日期计算器”程序完成
准备完了所有的基础技能后就让我们来完成开始说的那个任务吧我们来思考一下哈首先我们需要有一个循环循环每一次让计算机帮我们计算一次下一天的日期。每次在计算下一天日期的过程中先让日子加1判断是否跨月如果跨过了一个月份就让日子从1开始让月份加1再判断是否跨年如果跨年了就让月份从1开始年份加1。
如上的过程中有一个关键问题需要你注意就是2月份的月份天数的计算方法咱们来简单回顾一下闰年的规则年份满足以下其中一条即为闰年
* 能被4整除但不能被 100 整除;
* 能被 400 整除。
如果把闰年的规则翻译成逻辑判断,应该是下面这个样子:
```
if ((year % 4 == 0 && year % 100 != 0) || year % 400 == 0) ...
```
下面就让我们把思路过程转换成程序过程:
```
#include <stdio.h>
int main() {
int y, m, d, X; // 定义存储 年月日 和 X 的变量
scanf("%d%d%d", &y, &m, &d); // 读入年月日
scanf("%d", &X); // 读入 X 值
for (int i = 0; i < X; i++) { // 循环 X 次,每次向后推一天
d += 1;
switch (m) {
case 1:
case 3:
case 5: { // 第一部分逻辑
if (d > 31) d = 1, m += 1;
if (m == 13) m = 1, y += 1;
}; break;
case 4:
case 6: { // 第二部分逻辑
if (d > 30) d = 1, m += 1;
} break;
case 2: { // 第三部分逻辑
if ((y % 4 == 0 && y % 100 != 0) || y % 400 == 0) {
if (d > 29) d = 1, m += 1;
} else if (d > 28) {
d = 1, m += 1;
}
} break;
}
}
printf("%d %d %d\n", y, m, d);
return 0;
}
```
上面这段程序是个半成品只处理了前6个月的情况并且用到了**switch…case 的分支结构**,与 if 结构类似,都是用于做逻辑分支判断的。关于这部分的内容,给你留个小作业,自学一下 switch…case 分支结构然后按照自己的理解补全上述程序使得上述程序可以处理一年中12个月的全部情况。
虽然这个程序中有一部分内容需要你进行自学,可你也不要担心,我还是会跟你详细解释上述程序设计的思路。读入部分的代码,相信你现在已经可以很好的掌握了,这一部分就不展开解释了。程序整体设计中,是用 for 循环包裹了 switch…case 结构for 循环负责循环 X 次,每次在循环内部,都将对日子变量 d 进行加 1 操作,而在 switch…case 结构内部,主要是处理跨月和跨年的问题。
你会看到 switch…case 结构中主要分成三部分逻辑第一部分逻辑主要处理天数为31天的月份由于12月也是31天所以当本月是12月并且发生了跨月变成了13月说明是到了下一年的 1 月,需要将年份 +1月份置为 1 月。第二部分逻辑主要处理天数为30天的月份。第三部分逻辑主要处理 2 月份的情况在这里程序中分成两种情况来讨论闰年和非闰年闰年的时候判断日子是否超过29天非闰年判断日子是否超过28天。
我保证,在你尝试补全上述程序的过程中,你会发现,上述程序易于修改和补全,你要是能试着将上述程序修改成 if 分支结构,那就更好了。这样你将对上述程序结构的美,会感受的更深刻。
## 课程小结
最后呢,来总结一下今天所学的重点。今天呢,我们主要学习了两种程序流程控制结构,一种分支结构,主要以 if 语句为代表,另一种循环结构,以 for 循环和 while 循环为代表。如果说你只想记住几点的话,那么应该是以下几点:
1. 熟练掌握分支和循环结构的执行顺序,这一点很重要。
2. if 语句,首先判断条件表达式的真假,如果为真,则执行 if 里面的语句。
3. for 循环,分成四部分,其中②④③部分,构成了一个循环,第①部分是用做初始化的。
4. 所谓一条语句的概念,包括了空语句,单一语句,复合语句和结构语句。
以上这 4 点要牢记哦,尤其是其中的分支和循环结构的执行顺序,因为掌握和理解了程序的执行顺序,才是分析程序,理解程序的第一步。
好了,今天就到这里了,下期我将带你来做一个小总结,我将带你学习一个有趣的圆周率的计算方法,我是胡光,我们下期见。