Lesson 5 浮点数
在 4.2 中我们提到了 int
这个数据类型,它代表整数型数据。我们当前的程序输入输出的都是整数,但如果我们的数据不是那么 “刚好” 呢?请看接下来的一个问题:
-
问题描述:我们一般使用公制单位(米)测量身高,但是美国民间却固执地使用英制单位(英尺和英寸)测量身高。现在请你编写一个程序,将英制单位换算为公制单位。
换算关系为:
示例:五尺七寸换算为公制单位为 米
示例已经向我们抛出了一个难题:输出的数据可不是整数啊,它是小数啊!这下怎么办?
先来看第一版程序。为了让程序顺利输出,我们要改一下 printf
的格式字符串。
1 | printf("请分别输入身高的英尺和英寸," |
printf
的 %d
被我们改成了 %f
,表示这里输出的是一个小数。实际上,这里的 f 表示单精度浮点数(float)。
输出的问题搞定,现在来试一试示例:
1 | >>请分别输入身高的英尺和英寸,如输入"5 7"表示5英尺7英寸:5 7 |
???我们的结果和示例不一样啊😨,哪里出现了 bug?编译器你说句话啊。
在 3.2 我们讲到处理程序错误时,特地说明了编译器检查的是语法错误。编译器不检查逻辑错误。如果程序在逻辑上存在错误而语法上正确,程序依然可以通过编译流程。
不要慌,现在进行代码审查,看看是哪一行可能出错。
通过审查,我们发现前 1~5 行代码应该都无问题,出问题的最有可能是第 6 行的 printf
。是哪里出问题了呢?下面是分析:
-
在 C 语言中,两个整数的运算的结果只能是整数。例如,将 3.6 的 Calculator 程序修改一下,计算
10 / 3 * 3
,得到结果为 9。 -
现在把
10
改成10.0
,输出%f
试试。现在的结果为10.000000
。在 C 语言中,10
和10.0
是完全不同的数。10
是整数,10.0
是浮点数。 -
既然整数间的运算只能得到整数,那么计算机要如何处理像 7÷12 这样不是整数的结果呢?答案是强制转换为整数,只取整数部分,小数点后面的数字直接舍去。这导致
inch / 12
的结果变成了 0,最终导致整个程序输出错误。
计算机内部使用浮点方式表达非整数(包括分数和无理数)。“浮点” 的本意就是 “浮动的小数点”,因此浮点数就是带小数点的值。
另一种表达非整数的方式叫做定点,即 “固定的小数点”。具体可参考定点数 (fixed-point number) 的表示方法,这里不展开讲。
好,我们现在已经知道出错的原因,现在修复一下 bug:
1 | printf("身高是%f米。\n", ((foot + inch / 12.0) * 0.3048)); |
在 C 语言中,如果出现整数和浮点数的混合运算,C 会将整数转换为浮点数,然后进行浮点数运算,得到浮点数结果。
这个 bug 还有另一种修改方案:
1 | double inch; |
我们将 inch
定义为双精度浮点数(double),原理和效果与上面是一样的。注意此时格式字符串要改为 %lf
,即 “long float”。
整数不能表示带小数部分的数,整数间运算结果还是整数,适用范围明显比浮点数小,那么为什么不全用浮点数呢?其实是为了运行效率。在计算机中,整数的占用空间较小,运算速度也较快,因此在保证结果正确的前提下,多用整数是一个更划算的做法。
Lesson 6 表达式
6.1 运算符和算子,数据和计算 —— 程序的本质
在 4.2,我们提到了表达式。表达式是一系列运算符和算子的组合,用来计算一个值。** 运算符(operator)** 是指运算的动作,** 算子(operand,也叫 “操作数”“运算数”)** 是指参与运算的值,这个值可能是常数,也可能是变量,还可能是一个方法的返回值。
在 3.6 总结的常用算术运算符中,我们看到一个不属于四则运算的运算:取余。
数学运算符 | C 语言对应符号 | 意义 |
---|---|---|
+ | + |
加 |
- | - |
减 |
× | * |
乘 |
÷ | / |
除 |
% |
取余 | |
() | () |
括号 |
余数的概念我们是知道的。两个数整除后,剩下的不能被除数整除的数即为余数。取余操作就是要去这个余数。
根据 Arithmetic operators - cppreference.com,如果如果商 a/b
可以在结果类型中表示,那么 (a/b)*b + a%b == a
(即 a%b == a - (a/b)*b
)。如果第二个操作数 b
是零,则行为未定义。如果商 a/b
无法在结果类型中表示,则 a/b
和 a%b
的行为是未定义的。
注意:取余运算符不适用于浮点数。
但取余有什么用呢?现在举个例子说明。
- 问题描述:正在军训的小 L 即将开始下午的训练。看着不近人情的时间安排,小 L 叹息一声,想知道自己还能午睡多长时间。请你写一个计算时间差的程序,输入两个时间(每个时间分别输入小时和分钟的值,前一个时间为起始时间,后一个时间为结束时间,两个时间均在同一天),然后输出两个时间的差,按输入方式输出。
为了写这个程序,我们需要思考两个东西:数据和计算。数据思考怎么放、怎么用和怎么读,计算思考怎么算。
数据方面比较好写,我们可以先写:
1 | int hour1, minute1; |
接下来我们思考计算方面,也就是我们说的算法。
- 1. 最直接的方式就是小时减小时,分钟减分钟。但往往会因为出现 “分钟借位” 的情况而出错(比如负的分钟数),因此暂时不可行。
- 2. 再举个例子,我们怎么计算 1m10cm 和 20cm 的长度差?要么 1m10cm 换算为 110cm,要么 20cm 换算为 0.2m、1m10cm 换算为 1.1m。 不管是哪种做法,底层逻辑都是统一单位、统一十进制。同一个单位且为十进制的数据才有计算意义。对于时间差计算,也是一样的道理。
分钟和小时不是同一个单位,换算关系是 60 进制而不是十进制,需要统一单位。
这里我们将小时转换为分钟来计算时间差,规避使用浮点数。原因在 [Lesson5](#Lesson 5 浮点数) 的末尾有讲。
1 | int t1 = hour1 * 60 + minute1; |
这时的时间差只用了分钟来表示,跟题目要求的格式还是不一样,那么现在就要将分钟再转换为小时。
1 | printf("时间差是%d小时%d分", t / 60, t % 60); |
t / 60
很好理解,由于整数间运算得到整数,因此该操作代表整除,用来计算小时部分。而 t % 60
就是取余操作了,用来计算换算后剩余的分钟数。
6.2 运算符优先级
先来一个例子。
- 问题描述:请你写一个程序,输入两个整数,输出它们的平均值。
1 | int a, b; |
和我们小学学的一样,乘除法的优先级比加减法的优先级要高,因此我们要先进行加减法运算时,需要给加减法套一个括号给它 “升级”。运算符优先级的差别往往会将运算导向不同的结果。
接下来是一些算术运算符的优先级排列:
优先级 (数字越小越优先) | 运算符 | 运算 | 结合关系 | 举例 |
---|---|---|---|---|
1 | + |
单目不变 | 自右向左 | a * +b |
1 | - |
单目取负 | 自右向左 | a * -b |
2 | * |
乘 | 自左向右 | a * b |
2 | / |
除 | 自左向右 | a / b |
2 | % |
取余 | 自左向右 | a % b |
3 | + |
加 | 自左向右 | a + b |
3 | - |
减 | 自左向右 | a - b |
4 | = |
赋值 | 自右向左 | a = b |
这里补充一下单目的知识。我们在 [6.1](#6.1 运算符和算子,数据和计算 —— 程序的本质) 中提到算子的概念,而 “目” 就是用来指代算子的数量的。单目说明该运算只有一个算子。单目运算的优先级要高于双目运算
赋值在 C 语言中属于运算,也有结果,赋值的结果就是赋的那个值。执行 a=b=6
时,计算机其实是先执行 b=6
,再执行 a=b
。为了保证其他运算正常执行,赋值运算的优先级最低。
既然赋值是个运算,那么就有人想了:🤓☝️诶,那我要给一个新变量赋值的时候,把赋值式直接写在运算式子里不就好了?于是 TA 写出了这行代码:
1 | int c = 1 + ( b = a ); |
这种写法叫做 “嵌入式赋值”。嵌入式赋值不利于阅读,还容易出错,因此实践中不采用这种写法。这里的示例还比较好读,接下来的一个示例就会让你崩溃。
完整的运算符优先级表见 C Operator Precedence - cppreference.com。有趣的一点是,C/C++ 标准没有规定运算符优先级,优先级是从语法角度定义的。
某个教学用编程语言强制要求学生使用括号手动区分优先级。换句话说,对于 3 + 4 * 5
,该语言不会按 “先乘除,后加减” 的方式解析,而是先加后乘(即顺序解析,因为 “加” 和 “乘” 在同一层)。如果要实现 “先乘后加”,需要 3 + (4 * 5)
。
这其实是为了培养一种编程习惯:消除歧义,不要让编译器猜测意图。
因此,如果搞不清楚一个表达式将如何被编译器解析,那就自己加括号手动 “解析”。
接下来讲讲结合关系。结合关系可以理解为阅读顺序。运算的一般结合关系是自左向右,但单目不变 / 相反以及赋值的结合关系是自左向右。
现在请根据运算的结合关系,判断以下两个表达式的运算顺序:
1 | //Example 1 |
Example 1 的顺序很好理解,计算机先计算 c + 3
,然后依此进行 b = c + 3
、a = b
和 result = a
。
Example 2 就比较难以理顺了,这就是为什么不使用嵌入式赋值的一个案例。它的顺序是这样的:由于括号的升级,计算机先计算 result = result * 2
和 result = 3 + result
。乘法的结合顺序是自左向右,因此先计算 result = result * 2
,再计算 result = 3 + result
。赋值运算优先级最低,先计算 result = result * 2
再赋值,然后按同理计算 result = 3 + result
。 result = 3 + result
的值再乘以 6,最终结果赋值给 result
。(好累啊 QAQ)
6.3 交换变量
问题起手!
- 问题描述:现有两个已赋值的变量
a = 5
、b = 6
,请你交换a
、b
变量的值。
有没有人写 a = b; b = a;
的?在 4.2 中,我们已经说明了计算机的赋值运算符代表的是一种动作,而不是关系。程序是按步执行的,因此计算机会先将 b
的值赋给 a
,再将赋值完后 a
的值赋给 b
,结果使 a
和 b
都得到 b
的初始值。
那应该如何处理?举一个生活化的例子:假设我有一杯咖啡和一杯茶,我希望交换一下它们用的杯子,由于我手比较笨,不可能让咖啡和茶在空中交换落进杯子里,于是我们需要引入第三个杯子,把咖啡或茶的杯子先空出来让另一方先交换,再把第三个杯子中的饮品倒进目标杯子中。有玩过汉诺塔的同学应该可以理解是什么意思。“第三个杯子” 就被称为中间量,这个过程叫做交换。
按这个思路,我们可以写出程序:
1 | int a = 5; |
在程序运行时,我们可能会想知道运算和变量值是否正确,这时需要调试代码。
在调试前,你可以现预设一些验证值。常用的验证值有:1、边界数据,比如有效范围两端的数据以及特殊的倍数等;2、个位数;3、整十数;4、0;5、负数。
有时间的话,你也可以人脑模拟计算机的运行,在纸上列出所有的变量,随着程序的运行不断计算变量的值,得到程序的最终运算结果。
接下来我们移到计算机上来调试代码。
调试的一种直接方法就是在一组运算后输出当前变量值,即在适当位置插入 printf
。当然,这些插入的代码是面向开发人员的,普通用户正常运行程序时,这些代码应该被禁用或删除。
但现在我们用不着,毕竟我们的项目规模较小,而且如果忘记屏蔽掉这些代码,程序会向用户输出要求外的东西。这时我们可以使用 IDE 的 “调试” 功能。IDE 的调试可以按步运行程序,而不是等程序运行完后给个结果;变量的值可以随时查看,不用写调试代码。
在使用调试功能前,我们要为代码设置断点,告诉程序停一下,由开发人员决定是否继续执行接下来的代码。要设置断点,可以点击要停止的代码行的数字,此时 IDE 会用红点标注等方式突出断点位置。
交换就是这样。引入中间变量,然后换来换去。这是一个成熟的套路,就像英语中的句型,用进去就可以解决某个问题。套路的积累也和句型一样,靠的是阅读。阅读其他人的代码,学习对方在解决某类问题时的常见写法是什么样的,然后去模仿,去套用。
6.4 复合赋值和递增递减
复合赋值将算术运算和赋值结合在一起,具有节约空间的好处。
以下两个式子等价:
1 | total = total + 5; |
复合赋值的运算关系是从右往左,先计算完右边的运算,再结合左边的运算。
我们还有一对比较特殊的运算符 “++” 和 “–”,叫做递增递减运算符。这两个运算符都是单目运算符,且算子必须是变量。运算效果就是给这个变量 + 1 或者 - 1.
以下三个式子等价:
1 | count++; |
递增递减运算符既可以放在变量的前面,叫做前缀形式;也可以放在变量的后面,叫做后缀形式。两种形式都可以给变量递增递减,但表达式的结果不一样。a++
的结果是 a 加 1 以前的值,而 ++a
的结果是 a 加 1 以后的值。
可以这么来看:递增递减在变量后面的,先输出变量的值,然后再给变量递增递减;递增递减在变量后面的,先给变量递增递减,再输出变量值。
递增递减运算符有它的发展历史。原先是机器上有两条特殊的机器指令:INC
(递增)和 DEC
(递减)。当时的 C 语言编译器遇到 ++
和 --
就知道要使用 INC
和 DEC
,从而加速程序运行。但到了今天,C 语言编译器已经可以自动将 a = a + 1
识别为递增运算,而有些 CPU 也取消了 INC
和 DEC
指令,所以现在写 ++
和 --
只是出于习惯和书写方便而已。
最好不要将递增递减运算符组合进表达式里。单纯写 a++
和 a--
还是可以的,但一旦组合进表达式里就会遇到运算顺序的问题。比如,下面三个表达式的结果分别是什么?
1 | ++i++; |
第三个表达式的一种解释是这样的:
-
c++
是后缀自增操作符,表示使用 c 的当前值进行计算,然后将 c 的值增加 1。 -
--e
是前缀自减操作符,表示首先将 e 的值减 1,然后使用新的 e 值进行计算。/
是除法操作符,所以--e / -f
表示 e 减 1 后的值除以 -f(即 f 的负值)。 -
接下来是加法和减法操作,
c++ - d + --e / -f
表示使用 c 的原始值减去 d,然后加上 e 减 1 后的值除以-f
的结果。b +=
是复合赋值操作符,表示将 b 与表达式的结果相加,然后将结果赋值给 b。 -
最后,
a =
表示将整个表达式的结果赋值给 a。
所以整个表达式可以按以下步骤理解:
计算 --e / -f
,即 e 减 1 后的值除以 f 的负值。使用 c 的当前值,减去 d,然后加上第一步的结果。将第二步的结果加到 b 上,然后赋值给 b。将第三步的结果赋值给 a。
但实际上,这类问题没有唯一答案!因为不同编译器会对单行表达式的解析规则是不一样的,不会严格按照顺序执行运算。这涉及到编译原理,因此有些人为此辩护说 “为了了解底层”。但你看到这道题,会想到编译器是如何处理的吗?不,想到的是你如何处理。
还有人倒反天罡,指责编译器出错。对于这些人,我只能祝愿他们在以后的开发过程中不会遇到那些 “有错” 的编译器。
多个递增递减运算符的结合是未定义的。这种表达式由编译器自己决定如何处理。
1 | >i = ++i + i++; // undefined behavior |
——Order of evaluation - cppreference.com
正确的 C 语言程序不会包含未定义行为。当开启优化的编译器对存在未定义行为的程序进行编译时,会产生意料之外的情况。
6.5 习题课 1—— 三位数逆序输出
-
问题描述:请你写一个程序,该程序每次读入一个正三位数,然后输出按位逆序的数字。注意:当输入的数字含有结尾的 0 时,输出不应带有前导的 0,即输入 700,逆序后输出为 7.
-
输入格式:每个测试是一个三位的正整数。
输出格式:输出按位逆序的数。
-
输入样例:123
输出样例:321
解决这个问题,我们有三个步骤:分离、调换与拼合。我们首先要分离各位数字,然后调换头尾顺序,再拼合成一个新数字。
代码提示:
1 | int a, b, c // 分别代表百、十、个位数 |
如果我们需要前导 0 呢?可以直接写:
1 | printf("%d%d%d", a, b, c); |
也可以写:
1 | printf("%03d", x); |
这表示:按宽度为 3、右对齐输出。若不够 3 位,前面补 0.
Lesson 7 判断初步
7.1 判断语句 —— 用 if
做判断
在 [6.1](#6.1 运算符和算子,数据和计算 —— 程序的本质) 中时间差计算程序中,我们发现,直接小时减小时、分钟减分钟会出现分钟错位的状况。当时我们采用单位转换的方式巧妙化解了问题。但如果我们一定要用前一个方法呢?
我们已经说了,前一个算法会出现分钟错位的情况,那如果我专门处理这种情况呢?别的情况是不是就可以直接用小时减小时、分钟减分钟了?
所以我们要用 if
语句,把不同的情况按照一定的标准分开处理,这叫做条件判断。在时间差计算程序中,我们的分类标准是看分钟差有没有出现负数。
下面是示例:
1 | int ih = hour2 - hour1; |
小括号里写判断条件,大括号里写符合条件后要执行的代码。即
1 | if (成立条件) |
当 im < 0
时,程序判定数据符合 if
分支的条件,执行 if
内的代码后再执行 printf
;如果 im > 0
,程序判定数据不符合 if
分支的条件,就会跳过这一块的代码,直接执行 printf
。
7.2 判断条件 —— 关系运算符
关系运算符用于计算两个值之间的关系。常用的关系运算符有以下几个:
运算符 | 意义 |
---|---|
== [1] |
相等 |
!= [2] |
不相等 |
> |
大于 |
>= [3] |
大于或等于 |
< |
小于 |
<= [4] |
小于或等于 |
关系运算只有 0 和 1 两个运算。当两个值的关系符合关系运算符的预期时,关系运算的结果为 1,否则为 0.
关系运算符的优先级比算术运算符的低,但比赋值运算符的高。在关系运算符中,判断是否相等的 **==
和 !=
的优先级比其他关系运算符低 **,而连续的关系运算是从左往右进行的。
1 | 5 > 3 == 6 > 4 |
上面的式子先判断 5 > 3
,再判断 6 > 4
,最后判断这两个运算的值是否相等。
7.3 我要找零吗?—— 判断单个特殊情况
- 问题描述:
找零是这样的,学生只需要拿出一张百元大钞就行了,而小卖部老板要考虑的就多了。现在请你为小卖部设计一个找零计算器。
这是一个比较泛的需求,让我们想想平常小卖部老板找零的流程,看看如何让计算机代劳。
- 老板接收顾客支付的纸币,计算与所购商品的总价的差值。若多出,则找出多出的钱数并报出;若不够,则告诉顾客钱不够。
- 从计算机程序的角度看,这意味着程序需要读取两个输入,然后进行一些计算和判断,最后输出结果。
先解决输出和计算的问题。
1 | int price = 0; |
接着我们来写判断的部分。写判断时,我们需要知道数据符合某一标准时,程序需要执行哪些操作。这里我们可以画流程图来辅助理解。

由上面的流程图,我们就知道了判断部分代码的写法:
1 | if (bill >= price) |
但这里还没有完全处理好问题的需求。这一点我们下一节再讲。
再看一个例子。
- 问题描述:35 岁前的人生是年轻的、美好的!希望各位珍惜年华!现在请你写一个程序,输入用户年龄,显示对应的文案。
用流程图表示是这样的:

编写成程序是这样的:
1 | const int MINOR = 35; |
第 5 行不是必要的,但是是提升人机交互体验的一种做法。通过复述用户输入结果,用户可以知道自己的输入有没有被程序正确读取,同时确认自己的输入是否正确。
7.4 找零还是再要钱?—— 隔离输出不同的分支
在 [7.3](#7.3 我要找零吗 —— 判断单个特殊情况) 中,我们设计了一个找零计算器,并写了顾客支付超出时的分支。但顾客付的钱不够呢?
我们当然不能直接在末尾写上 printf(“你的钱不够”)
,因为这样做的话,这一行代码是不受 if 控制的,不论什么情况都一定要输出。想一想,程序上一秒还说要找零,下一秒就说钱不够,顾客表示:“是不是你把钱偷了😠”。这显然不合理。
于是我们就需要 else
语句来隔离不符合 if
语句块条件的情况。“else” 就是 “否则的话”。基于此,我们可以:
1 | if (bill >= price) |
用流程图表示是这样的:

不过有时我们不需要 else
语句也能隔离输出不同的分支。下面来看一个例子:
- 问题描述:输入两个整数,输出较大的那个数。
我们有三个隔离方案。
Case1(复用 if
语句):
1 | int max = 0; |
Case2(if...else...
语句):
1 | int max = 0; |
Case3:
1 | int max = b; |
三种方案各有优劣。在实践中,推荐采用 Case2 的写法,因为它足够清晰,易读性好,又不会像 Case1 那样太低级。Case3 可以被叫做假设法,是一种比较取巧的做法。它在效率上占优,但有时不够清晰,易读性相对较差。
7.5 再探 if 语句
现在我们要对 if 语句做一个详细的定义了:一个基本的 if 语句由一个关键字 if
开头,跟上在括号里的一个表示条件的逻辑表达式,然后是一对大括号 {} 之间的若干条语句。如果表示条件的逻辑表达式的结果不为 0,那么就执行大括号中的语句;否则,跳过这些语句不执行,而继续下面的其他语句。
注意 if 语句中,if
关键字所在的那一行是没有 ;
的,而下面的语句缩进并有 ;
。这表明这条语句是 if 语句的一部分,if 语句拥有和控制这条语句,决定它是否要执行。
if...else
语句可以不写大括号,但 if
与 else
只有其所在行的下一行语句的控制权,即只能运行单行代码。例如:
1 | if (a > b) |
即使不满足 a > b
,printf
语句仍然会执行,因为它不受 if
语句控制。
写大括号的优势是显而易见的:可以执行多行代码、易于和其他代码区分,可读性好。我们建议写上大括号 {}
,即使只有单行语句。
-
问题描述:作为一名苦逼的
帕鲁大学生,小 G 在暑假时进厂打了工。该厂的正常时薪为 8.25 元,每周标准工作时长为 40 小时,超过 40 小时后的工作时间视为加班时间,时薪为原先的 1.5 倍。请你写一个程序,输入小 G 本周的工作时长(保证为整数),输出小 G 本周的薪水。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16const double RATE = 8.25;
const int STANDARD = 40;
double pay = 0.0;
int hours;
printf("请输入工作的小时数:");
scanf("%d", &hours);
printf("\n");
if (hours > STANDARD)
{
pay = STANDARD * RATE + (hours - STANDARD) * (RATE * 1.5);
}
else
{
pay = hours * RATE;
}
printf("应付工资:%lf\n", pay); -
问题描述:请你写一个程序,判断某场考试学生成绩是否及格。该考试保证为百分制且分数为整数,及格线设为 60 分。
1 | const int STANDARD = 60; |
7.6 我的面前有好多条路 ——else if
语句
-
问题描述:请你写一个程序,输入一个 4 位及以下的正整数,然后输出这个整数的位数。
先从算法角度思考这个问题:
- 对于人来说,这件事情就是眼睛盯一下就可以得出结果的事。但对于计算机而言,天哪,那是不可能的.jpg
- 计算机用的一种方式是通过判断数的范围来作为它的位数。例如,352∈[100,999],则 352 是一个三位数。
如果我们通过范围判断位数,那么我们起码要准备三个分支(还有一个分支可以用假设法优化掉)。可
if...else...
语句只支持两个分支,怎么办?你可能会觉得这只是 “多写几个
if
语句” 的事,但几个if
语句间是互相独立的,它们的判断也是独立的,最终结果由最后的那个if
决定。也就是说,你可能试了几个不同位数的数,但程序的惊天智慧会认为它们的位数是一样的。为了应对一个判断、更多分支的情况,我们要使用
else if...
语句。else if
和if
一样需要条件,使用起来也和if
差不多。与几个if
语句并排不同的是,只要其中一个分支符合条件,那么接下来的其他分支便不再进行判断。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16int x;
int n = 1;
scanf("%d", &x);
if (x > 999)
{
n = 4;
}
else if (x > 99)
{
n = 3;
}
else if (x > 9)
{
n = 2;
}
printf("%d\n", n);用流程图表示:
Tip你有没有注意到我们是先判断 x>999,然后从高到低判断?有的人会从低到高判断,即先判断 x>9,这时需要明确范围,将判断区间的左右值都写出来,即 9<x<99(但 C 语言不能这么写,到逻辑运算我们会讲)。否则所有大于 9 的数都会进入 n=2 的分支,其他分支不再进行判断。
因此我们建议在判断数据下限(x > a)时,采用从高到低的判断形式。同理,判断数据上限(x < a)时,应该采用从低到高的判断形式。
Lesson 8 循环初步
8.1 如何处理重复性作业?
在 [7.6](#7.6 我的面前有好多条路 ——else if 语句) 中我们写了一个判断四位及以下数的位数的程序。现在我们问题的范围变成全体正整数,还能用上面所用的方法吗?
用户输了一个五位数,行,加一个 else if
;用户输了一个六位数,行,加一个 else if
;用户数了一个七位数,行,加一个 else if
…… 用户输了一个惊天大数,草,加个毛线!
很显然这种水多加面的做法在一定范围内还可以用,但一旦遇到范围不定的情况,用户输入就很容易超出你的预想,导致程序错误输出(酒吧笑话.jpg)
这种情况怎么办?我们先来看看人类是怎么做的。
- 对于 1213243243543549877 这个数,你知道它是几位数吗?这就不能眼睛盯一下就得出结果了,需要数一下。你会怎么数数?从左往右数,数完一个数就忽略(或者划掉)这个数,并默念 1、2、3……
- 其实计算机也可以这样做。借鉴 [6.5](#6.5 习题课 1—— 三位数逆序输出) 的做法,让计算机逐位分离数字,每分离一位,就给存储位数的变量 + 1.
- 但我们需要换一下方向,让计算机从右往左数数。我们可以不断地 **
/10
来去掉最右边的数 **,不断地划,直到没有数字可以划。
这个 “不断地划” 的过程,就是循环。循环用于处理内容相同而重复的工作,只要满足一定条件,循环体就可以不断地重复执行一段代码,直到不再满足条件。
这是一个 while
循环的示例:
1 | int x; |
输入 x 后,while
函数会先判断是否满足 x > 0
,满足后执行 {}
内的代码,执行完后再次判断条件,满足后再次执行 {}
内的代码,然后再次判断条件。条件不满足时,停止循环,执行下面的 printf
。
注意循环体内要有改变条件的机会,否则循环会一直进行下去,变成死循环,你的程序会无法自动退出!
8.2 while
循环 —— 条件优先,循环次数不定
while
循环和 if
判断在语法上很相似,只是换了一个关键字,但它们在原理上是不一样的。while
循环的意思是:当条件满足时,不断地重复循环体内的语句。
在循环执行之前需要判断是否满足条件,因此有可能循环一次也没有被执行。条件成立是循环继续的前提。
while
循环的基本模式是这样的:

8.3 do-while
循环 —— 循环体优先,循环次数不定
do-while
循环在首次进入循环时不做检查,而是在执行完一轮循环体的代码后,再检查循环的条件是否满足,如果满足则继续下一轮循环,不满足则结束循环。
和 while
循环不一样的是,do-while
循环至少执行一次,然后再判断条件。
do-while
循环的语法是这样的:
1 | do |
基本模式是这样的:

8.4 for
循环 —— 条件优先,循环次数一定
- 问题描述:请你写一个程序,输入,输出。其中 为 n 的阶乘,。
从数据角度考虑:
- 读取用户输入需要一个
int
类型的n
,然后计算的结果也需要一个变量存储,如int
类型的factor
,在计算中还需要一个变量从 1 递增到n
,可以是int
类型的i
。
从计算角度考虑:
-
我们可以使用
while
循环来处理这个问题:1
2
3
4
5
6
7
8
9
10
11int i;
scanf("%d", &n);
int factor = 1;
int i = 1;
while (i <= n)
{
factor *= i;
i++;
}
printf("%d! = %d\n", n, factor); -
但在 C 语言中我们还有另一个语句可以替代:
1
2
3
4for (int i = 1; i <= n; i++)
{
factor *= i;
}
这个替代的语句叫做 for
循环。for
循环多用于计数循环:设定一个计数器,初始化它,然后在计数器到达某值之前,重复执行循环体,而每执行一轮循环,计数器值以一定步进进行调整,比如 + 1 或 - 1。
for 可以理解为 “对于”,所以像 for (int i = 0; i > 0; i--)
就可以被理解为:对于一开始的 i = 10
,当 i > 0
时,重复做循环体,每一轮循环体在做完循环体内语句后,使得 i--
。
由于计数器(循环控制变量)只在循环里被用到,所以我们可以在 for 语句里定义计数器变量。
有时我们也可以不用再定义一个计数器。如果表达式内已经有按照期望速度步进的变量,我们可以让这个变量兼任技术的作用。
1 | int n, factor = 1; |
for 循环的语法是这样的:
1 | for (<初始化计数器>; <条件>; <改变计数器>) |
计数器变量可以在
for
语句外定义,也可以在for
语句内定义。不同的定义位置会影响计数器的作用域。简单来讲,for
语句内定义的计数器不可用于循环体外,for
语句外定义的计数器在循环体内外都可以使用。
注意循环次数。对于
for (i = 0; i < n; i++)
来说,它的循环次数为 n,而循环结束后,i 的值是 n。循环控制变量是初始化为 0 还是 1、条件是 i < n 还是 i <= n,对循环次数、循环结束后变量的值都有影响。
基本模式是这样的:

实际上,for 循环可以视为 while 循环的一种变体。例如,以下两段代码等价。
1 | for (int i = 1; i <= n; i++) |
1 | int i = 1; |
Lesson 9 逻辑类型和运算
9.1 逻辑类型
逻辑类型也叫布尔(bool)类型,也是一个数据类型。布尔类型只有两个值:true
和 false
。但在 C 语言中,bool
类型的数据只能当作 int
类型来输入输出,也就是说,不能直接输入输出 true
和 false
,而只能输入输出非 0 整数(一般是 1)和 0.
C 语言本身是没有 bool
类型的,需要#include <stdbool.h>
后才能使用。
也可以使用关系运算来为 bool
类型变量赋值,这个值同样存储为整数。
9.2 逻辑运算
逻辑运算是对逻辑量进行的运算,结果只有 0 或 1。其中,逻辑量是关系运算或逻辑运算的结果。
常见的逻辑运算有三种:
运算符 | 描述 | 示例 | 结果 | 优先级 (数字越小越优先) |
---|---|---|---|---|
! | 逻辑非 | !a | 如果 a 是 true,运算结果为 false;如果 a 是 false,运算结果为 true | 1 |
&& | 逻辑与 | a && b | 当且仅当 a 和 b 都是 true,运算结果为 true,否则为 false | 2 |
|| | 逻辑或 | a || b | 只要 a 和 b 中有一个是 true,则运算结果为 true;如果 a 和 b 都是 false,则运算结果为 false | 3 |
现在我们有一个需求,要求用 C 中的表达式表示数学中的区间,如 x∈(4,6)。要怎么写?
直接写 4 < x < 6
吗?在 C 语言中,这将被视为两个关系运算,先运算 x > 4
,再用这个运算的结果(0 或 1)和 6 比较,最终结果必定是 1(true
),这肯定不对。
我们要写的是 x > 4 && x < 6
,即数学中的交集。
我们还有几个例子以供理解:
- 判断一个字符 c 是否为大写字母:
c >= ‘A’ && c <= ‘Z’
- 取并集:
index < 0 || index > 99
!age < 20
:!
是单目运算符,优先级比作为双目运算符的<
要高,因此计算机会先计算!age
,再和 20 比较。由于!age
不是 0 就是 1,所以这个表达式的值必定为 1.!done && (count > MAX)
:!
的优先级最高,因此先计算!done
,然后算count > MAX
(括号可以忽略),最后进行逻辑与计算。
以下是几类运算符优先级的对比:
优先级 | 运算符 | 结合性 |
---|---|---|
1 | () |
从左到右 |
2 | ! + - ++ -- [5] |
从右到左 |
3 | * / % |
从左到右 |
4 | + - |
从左到右 |
5 | < <= > >= |
从左到右 |
6 | == != |
从左到右 |
7 | && |
从左到右 |
8 | | |
从左到右 |
9 | = += -= *= /= %= |
从右到左 |
逻辑运算有短路机制,这是由它的从左到右的运算顺序决定的。如果左边的结果已经能够决定整个表达式的结果了,逻辑运算就不会做右边的运算。
例如,对于 a == 6 && b ==1
而言,如果 a == 6
是 false,那么这个表达式的结果一定是 false,计算机就不会去运算 b == 1
。
同理,对于 ||
,左边的运算结果是 true
的话,计算机就不会进行右边的计算。
完整的运算符优先级表可参考 C Operator Precedence - cppreference.com。
9.3 条件运算
条件运算也叫三元运算,基本格式是这样的:<条件> ? <条件满足时的表达式> : <条件不满足时的表达式>
。
1 | a = cond ? 1+2 : 1*2 |
条件运算符的优先级比赋值运算符的高,但比其他运算符的低。
条件运算符是从右到左结合的。
不建议在条件运算符中过分杂糅其他运算符,也不建议将几个条件运算符嵌套书写。
9.4 逗号运算
在 C 语言中,逗号用来连接两个表达式,并以其右边的表达式的结果作为它的结果。
逗号的优先级是所有表达式中最低的,所以它两边的表达式会先计算。
逗号的结合关系是从左到右,所以左边的表达式会先计算,而右边的表达式的值就作为逗号运算的结果。
逗号运算可以用在 for 语句中,用来处理多个计数器的情况。
1 | for (i = 0, j = 10; i < j; i++, j--) |