Lesson 5 浮点数

在 4.2 中我们提到了 int 这个数据类型,它代表整数型数据。我们当前的程序输入输出的都是整数,但如果我们的数据不是那么 “刚好” 呢?请看接下来的一个问题:

  • 问题描述:我们一般使用公制单位(米)测量身高,但是美国民间却固执地使用英制单位(英尺和英寸)测量身高。现在请你编写一个程序,将英制单位换算为公制单位。

    换算关系为:(英尺数 + 英寸数 ÷12)×0.3048= 米数 (英尺数 + 英寸数 ÷12)× 0.3048 = 米数

    示例:五尺七寸换算为公制单位为 (5+7÷12)×0.3048=1.7018(5+7÷12)× 0.3048 = 1.7018

示例已经向我们抛出了一个难题:输出的数据可不是整数啊,它是小数啊!这下怎么办?

先来看第一版程序。为了让程序顺利输出,我们要改一下 printf 的格式字符串。

1
2
3
4
5
6
printf("请分别输入身高的英尺和英寸,"
"如输入\"5 7\"表示5英尺7英寸:");//这个\"在格式字符串里表示需要输出"而非省略
int foot;
int inch;
scanf("%d %d", &foot, &inch);
printf("身高是%f米。\n", ((foot + inch / 12) * 0.3048));

printf%d 被我们改成了 %f,表示这里输出的是一个小数。实际上,这里的 f 表示单精度浮点数(float)

输出的问题搞定,现在来试一试示例:

1
2
>>请分别输入身高的英尺和英寸,如输入"5 7"表示5英尺7英寸:5 7
>>身高是1.524000米。

???我们的结果和示例不一样啊😨,哪里出现了 bug?编译器你说句话啊。

Tip

在 3.2 我们讲到处理程序错误时,特地说明了编译器检查的是语法错误。编译器不检查逻辑错误。如果程序在逻辑上存在错误而语法上正确,程序依然可以通过编译流程。

不要慌,现在进行代码审查,看看是哪一行可能出错。

通过审查,我们发现前 1~5 行代码应该都无问题,出问题的最有可能是第 6 行的 printf。是哪里出问题了呢?下面是分析:

  • 在 C 语言中,两个整数的运算的结果只能是整数。例如,将 3.6 的 Calculator 程序修改一下,计算 10 / 3 * 3,得到结果为 9。

  • 现在把 10 改成 10.0,输出 %f 试试。现在的结果为 10.000000。在 C 语言中,1010.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
2
double inch;
scanf("%d %lf", &foot, &inch);

我们将 inch 定义为双精度浮点数(double),原理和效果与上面是一样的。注意此时格式字符串要改为 %lf,即 “long float”。

整数不能表示带小数部分的数,整数间运算结果还是整数,适用范围明显比浮点数小,那么为什么不全用浮点数呢?其实是为了运行效率。在计算机中,整数的占用空间较小,运算速度也较快,因此在保证结果正确的前提下,多用整数是一个更划算的做法。


Lesson 6 表达式

6.1 运算符和算子,数据和计算 —— 程序的本质

在 4.2,我们提到了表达式。表达式是一系列运算符和算子的组合,用来计算一个值。** 运算符(operator)** 是指运算的动作,** 算子(operand,也叫 “操作数”“运算数”)** 是指参与运算的值,这个值可能是常数,也可能是变量,还可能是一个方法的返回值。

在 3.6 总结的常用算术运算符中,我们看到一个不属于四则运算的运算:取余

数学运算符 C 语言对应符号 意义
+ +
- -
× *
÷ /
% 取余
() () 括号

余数的概念我们是知道的。两个数整除后,剩下的不能被除数整除的数即为余数。取余操作就是要去这个余数。

Tip

根据 Arithmetic operators - cppreference.com,如果如果商 a/b 可以在结果类型中表示,那么 (a/b)*b + a%b == a(即 a%b == a - (a/b)*b)。如果第二个操作数 b 是零,则行为未定义。如果商 a/b 无法在结果类型中表示,则 a/ba%b 的行为是未定义的。

Warning

注意:取余运算符不适用于浮点数。

但取余有什么用呢?现在举个例子说明。

  • 问题描述:正在军训的小 L 即将开始下午的训练。看着不近人情的时间安排,小 L 叹息一声,想知道自己还能午睡多长时间。请你写一个计算时间差的程序,输入两个时间(每个时间分别输入小时和分钟的值,前一个时间为起始时间,后一个时间为结束时间,两个时间均在同一天),然后输出两个时间的差,按输入方式输出。

为了写这个程序,我们需要思考两个东西:数据计算。数据思考怎么放、怎么用和怎么读,计算思考怎么算。

数据方面比较好写,我们可以先写:

1
2
3
4
int hour1, minute1;
int hour2, minute2;
scanf("%d %d", &hour1, &minute1);
scanf("%d %d", &hour2, &minute2);

接下来我们思考计算方面,也就是我们说的算法。

  • 1. 最直接的方式就是小时减小时,分钟减分钟。但往往会因为出现 “分钟借位” 的情况而出错(比如负的分钟数),因此暂时不可行。
  • 2. 再举个例子,我们怎么计算 1m10cm 和 20cm 的长度差?要么 1m10cm 换算为 110cm,要么 20cm 换算为 0.2m、1m10cm 换算为 1.1m。 不管是哪种做法,底层逻辑都是统一单位、统一十进制同一个单位且为十进制的数据才有计算意义。对于时间差计算,也是一样的道理。

分钟和小时不是同一个单位,换算关系是 60 进制而不是十进制,需要统一单位。

这里我们将小时转换为分钟来计算时间差,规避使用浮点数。原因在 [Lesson5](#Lesson 5 浮点数) 的末尾有讲。

1
2
3
int t1 = hour1 * 60 + minute1;
int t2 = hour2 * 60 + minute2;
int t = t2 - t1

这时的时间差只用了分钟来表示,跟题目要求的格式还是不一样,那么现在就要将分钟再转换为小时

1
printf("时间差是%d小时%d分", t / 60, t % 60);

t / 60 很好理解,由于整数间运算得到整数,因此该操作代表整除,用来计算小时部分。而 t % 60 就是取余操作了,用来计算换算后剩余的分钟数。


6.2 运算符优先级

先来一个例子。

  • 问题描述:请你写一个程序,输入两个整数,输出它们的平均值。
1
2
3
4
int a, b;
scanf("%d %d", &a, &b);
double c = (a + b) / 2.0;
printf("%d和%d的平均数=%f\n", a, b, c);

和我们小学学的一样,乘除法的优先级比加减法的优先级要高,因此我们要先进行加减法运算时,需要给加减法套一个括号给它 “升级”。运算符优先级的差别往往会将运算导向不同的结果。

接下来是一些算术运算符的优先级排列:

优先级 (数字越小越优先) 运算符 运算 结合关系 举例
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。为了保证其他运算正常执行,赋值运算的优先级最低。

Warning

既然赋值是个运算,那么就有人想了:🤓☝️诶,那我要给一个新变量赋值的时候,把赋值式直接写在运算式子里不就好了?于是 TA 写出了这行代码:

1
int c = 1 + ( b = a );

这种写法叫做 “嵌入式赋值”。嵌入式赋值不利于阅读,还容易出错,因此实践中不采用这种写法。这里的示例还比较好读,接下来的一个示例就会让你崩溃。

完整的运算符优先级表见 C Operator Precedence - cppreference.com。有趣的一点是,C/C++ 标准没有规定运算符优先级,优先级是从语法角度定义的。

Note

某个教学用编程语言强制要求学生使用括号手动区分优先级。换句话说,对于 3 + 4 * 5,该语言不会按 “先乘除,后加减” 的方式解析,而是先加后乘(即顺序解析,因为 “加” 和 “乘” 在同一层)。如果要实现 “先乘后加”,需要 3 + (4 * 5)
这其实是为了培养一种编程习惯:消除歧义,不要让编译器猜测意图。
因此,如果搞不清楚一个表达式将如何被编译器解析,那就自己加括号手动 “解析”。

接下来讲讲结合关系。结合关系可以理解为阅读顺序。运算的一般结合关系是自左向右,但单目不变 / 相反以及赋值的结合关系是自左向右。

现在请根据运算的结合关系,判断以下两个表达式的运算顺序:

1
2
3
4
5
//Example 1
result = a = b = c + 3;
//Example 2
result = 2;
result = (result = result * 2) * 6 * (result = 3 + result);

Example 1 的顺序很好理解,计算机先计算 c + 3,然后依此进行 b = c + 3a = bresult = a

Example 2 就比较难以理顺了,这就是为什么不使用嵌入式赋值的一个案例。它的顺序是这样的:由于括号的升级,计算机先计算 result = result * 2result = 3 + result。乘法的结合顺序是自左向右,因此先计算 result = result * 2,再计算 result = 3 + result。赋值运算优先级最低,先计算 result = result * 2 再赋值,然后按同理计算 result = 3 + resultresult = 3 + result 的值再乘以 6,最终结果赋值给 result(好累啊 QAQ)


6.3 交换变量

问题起手!

  • 问题描述:现有两个已赋值的变量 a = 5b = 6,请你交换 ab 变量的值。

有没有人写 a = b; b = a; 的?在 4.2 中,我们已经说明了计算机的赋值运算符代表的是一种动作,而不是关系。程序是按步执行的,因此计算机会先将 b 的值赋给 a,再将赋值完a 的值赋给 b,结果使 ab 都得到 b 的初始值。

那应该如何处理?举一个生活化的例子:假设我有一杯咖啡和一杯茶,我希望交换一下它们用的杯子,由于我手比较笨,不可能让咖啡和茶在空中交换落进杯子里,于是我们需要引入第三个杯子,把咖啡或茶的杯子先空出来让另一方先交换,再把第三个杯子中的饮品倒进目标杯子中。有玩过汉诺塔的同学应该可以理解是什么意思。“第三个杯子” 就被称为中间量,这个过程叫做交换

按这个思路,我们可以写出程序:

1
2
3
4
5
6
7
int a = 5;
int b = 6;
int t;
t = a;
a = b;
b = t;
printf("a = %d, b = %d\n", a, b);
Note

在程序运行时,我们可能会想知道运算和变量值是否正确,这时需要调试代码。

在调试前,你可以现预设一些验证值。常用的验证值有:1、边界数据,比如有效范围两端的数据以及特殊的倍数等;2、个位数;3、整十数;4、0;5、负数。

有时间的话,你也可以人脑模拟计算机的运行,在纸上列出所有的变量,随着程序的运行不断计算变量的值,得到程序的最终运算结果。

接下来我们移到计算机上来调试代码。

调试的一种直接方法就是在一组运算后输出当前变量值,即在适当位置插入 printf。当然,这些插入的代码是面向开发人员的,普通用户正常运行程序时,这些代码应该被禁用或删除。

但现在我们用不着,毕竟我们的项目规模较小,而且如果忘记屏蔽掉这些代码,程序会向用户输出要求外的东西。这时我们可以使用 IDE 的 “调试” 功能。IDE 的调试可以按步运行程序,而不是等程序运行完后给个结果;变量的值可以随时查看,不用写调试代码。

在使用调试功能前,我们要为代码设置断点,告诉程序停一下,由开发人员决定是否继续执行接下来的代码。要设置断点,可以点击要停止的代码行的数字,此时 IDE 会用红点标注等方式突出断点位置。

交换就是这样。引入中间变量,然后换来换去。这是一个成熟的套路,就像英语中的句型,用进去就可以解决某个问题。套路的积累也和句型一样,靠的是阅读。阅读其他人的代码,学习对方在解决某类问题时的常见写法是什么样的,然后去模仿,去套用。


6.4 复合赋值和递增递减

复合赋值将算术运算和赋值结合在一起,具有节约空间的好处。

以下两个式子等价:

1
2
total = total + 5;
total += 5;//注意“+=”中间不要有空格

复合赋值的运算关系是从右往左,先计算完右边的运算,再结合左边的运算。

我们还有一对比较特殊的运算符 “++” 和 “–”,叫做递增递减运算符。这两个运算符都是单目运算符,且算子必须变量。运算效果就是给这个变量 + 1 或者 - 1.

以下三个式子等价:

1
2
3
count++;
count += 1;
count = count + 1;

递增递减运算符既可以放在变量的前面,叫做前缀形式;也可以放在变量的后面,叫做后缀形式。两种形式都可以给变量递增递减,但表达式的结果不一样。a++ 的结果是 a 加 1 以前的值,而 ++a 的结果是 a 加 1 以后的值。

Tip

可以这么来看:递增递减在变量后面的,先输出变量的值,然后再给变量递增递减;递增递减在变量后面的,先给变量递增递减,再输出变量值。

Info

递增递减运算符有它的发展历史。原先是机器上有两条特殊的机器指令:INC(递增)和 DEC(递减)。当时的 C 语言编译器遇到 ++-- 就知道要使用 INCDEC,从而加速程序运行。但到了今天,C 语言编译器已经可以自动将 a = a + 1 识别为递增运算,而有些 CPU 也取消了 INCDEC 指令,所以现在写 ++-- 只是出于习惯和书写方便而已。

最好不要将递增递减运算符组合进表达式里。单纯写 a++a-- 还是可以的,但一旦组合进表达式里就会遇到运算顺序的问题。比如,下面三个表达式的结果分别是什么?

1
2
3
++i++;
i++++;
a = b += c++ - d + --e / -f;

第三个表达式的一种解释是这样的:

  • 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。

但实际上,这类问题没有唯一答案!因为不同编译器会对单行表达式的解析规则是不一样的,不会严格按照顺序执行运算。这涉及到编译原理,因此有些人为此辩护说 “为了了解底层”。但你看到这道题,会想到编译器是如何处理的吗?不,想到的是如何处理。
还有人倒反天罡,指责编译器出错。对于这些人,我只能祝愿他们在以后的开发过程中不会遇到那些 “有错” 的编译器。

Note

多个递增递减运算符的结合是未定义的。这种表达式由编译器自己决定如何处理。

1
2
>i = ++i + i++; // undefined behavior
>i = i++ + 1; // undefined behavior

——Order of evaluation - cppreference.com

正确的 C 语言程序不会包含未定义行为。当开启优化的编译器对存在未定义行为的程序进行编译时,会产生意料之外的情况。

——Undefined behavior - cppreference.com


6.5 习题课 1—— 三位数逆序输出

  • 问题描述:请你写一个程序,该程序每次读入一个正三位数,然后输出按位逆序的数字。注意:当输入的数字含有结尾的 0 时,输出不应带有前导的 0,即输入 700,逆序后输出为 7.

  • 输入格式:每个测试是一个三位的正整数。

    输出格式:输出按位逆序的数。

  • 输入样例:123

    输出样例:321

解决这个问题,我们有三个步骤:分离、调换与拼合。我们首先要分离各位数字,然后调换头尾顺序,再拼合成一个新数字。

代码提示:

1
2
3
4
5
6
7
8
int a, b, c // 分别代表百、十、个位数
int x;
scanf("%d\n", &x);
a = x % 100; // 分离百位
c = x % 10; // 分离个位
b = x % 100 / 10; // 或者b = x / 10 % 10; 分离十位
x = c * 100 + b * 10 + a; // 调换与拼合
printf("%d", x);
Tip

如果我们需要前导 0 呢?可以直接写:

1
printf("%d%d%d", a, b, c);

也可以写:

1
printf("%03d", x);

这表示:按宽度为 3、右对齐输出。若不够 3 位,前面补 0.

参考:C 语言中 % d %.2d %2d %02d 的区别 - CSDN 博客


Lesson 7 判断初步

7.1 判断语句 —— 用 if 做判断

在 [6.1](#6.1 运算符和算子,数据和计算 —— 程序的本质) 中时间差计算程序中,我们发现,直接小时减小时、分钟减分钟会出现分钟错位的状况。当时我们采用单位转换的方式巧妙化解了问题。但如果我们一定要用前一个方法呢?

我们已经说了,前一个算法会出现分钟错位的情况,那如果我专门处理这种情况呢?别的情况是不是就可以直接用小时减小时、分钟减分钟了?

所以我们要用 if 语句,把不同的情况按照一定的标准分开处理,这叫做条件判断。在时间差计算程序中,我们的分类标准是看分钟差有没有出现负数。

下面是示例:

1
2
3
4
5
6
7
8
int ih = hour2 - hour1;
int im = minute2 - minute1;
if (im < 0)
{
im = 60 + im;
ih --;
}
printf("时差是%d小时%d分\n", ih, im);

小括号里写判断条件,大括号里写符合条件后要执行的代码。即

1
2
3
4
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
2
3
4
5
6
int price = 0;
int bill = 0;
printf("请输入商品总价:");
scanf("%d", &price);
printf("请输入顾客支付金额:");
scanf("%d", &bill);

接着我们来写判断的部分。写判断时,我们需要知道数据符合某一标准时,程序需要执行哪些操作。这里我们可以画流程图来辅助理解。

由上面的流程图,我们就知道了判断部分代码的写法:

1
2
3
4
if (bill >= price)
{
printf("应该找您:%d元\n", bill - price);
}

但这里还没有完全处理好问题的需求。这一点我们下一节再讲。

再看一个例子。

  • 问题描述:35 岁前的人生是年轻的、美好的!希望各位珍惜年华!现在请你写一个程序,输入用户年龄,显示对应的文案。

用流程图表示是这样的:

编写成程序是这样的:

1
2
3
4
5
6
7
8
9
10
const int MINOR = 35;
int age = 0;
printf("请输入你的年龄:");
scanf("%d", &age);
printf("你的年龄是%d岁。\n", age);
if (age < MINOR)
{
printf("年轻是美好的,");
}
printf("年龄决定了你的精神世界,好好珍惜吧。\n");

第 5 行不是必要的,但是是提升人机交互体验的一种做法。通过复述用户输入结果,用户可以知道自己的输入有没有被程序正确读取,同时确认自己的输入是否正确。


7.4 找零还是再要钱?—— 隔离输出不同的分支

在 [7.3](#7.3 我要找零吗 —— 判断单个特殊情况) 中,我们设计了一个找零计算器,并写了顾客支付超出时的分支。但顾客付的钱不够呢?

我们当然不能直接在末尾写上 printf(“你的钱不够”),因为这样做的话,这一行代码是不受 if 控制的,不论什么情况都一定要输出。想一想,程序上一秒还说要找零,下一秒就说钱不够,顾客表示:“是不是你把钱偷了😠”。这显然不合理。

于是我们就需要 else 语句来隔离不符合 if 语句块条件的情况。“else” 就是 “否则的话”。基于此,我们可以:

1
2
3
4
5
6
7
8
if (bill >= price)
{
printf("应该找您:%d\n", bill - price);
}
else
{
printf("您的钱不够\n");
}

用流程图表示是这样的:


不过有时我们不需要 else 语句也能隔离输出不同的分支。下面来看一个例子:

  • 问题描述:输入两个整数,输出较大的那个数。

我们有三个隔离方案。

Case1(复用 if 语句):

1
2
3
4
5
6
7
8
9
int max = 0;
if (a > b)
{
max = a;
}
if (b > a)
{
max = b;
}

Case2(if...else... 语句):

1
2
3
4
5
6
7
8
9
int max = 0;
if (a > b)
{
max = a;
}
else
{
max = b;
}

Case3:

1
2
3
4
5
int max = b;
if (a > b)
{
max = a;
}

三种方案各有优劣。在实践中,推荐采用 Case2 的写法,因为它足够清晰,易读性好,又不会像 Case1 那样太低级。Case3 可以被叫做假设法,是一种比较取巧的做法。它在效率上占优,但有时不够清晰,易读性相对较差。


7.5 再探 if 语句

现在我们要对 if 语句做一个详细的定义了:一个基本的 if 语句由一个关键字 if 开头,跟上在括号里的一个表示条件的逻辑表达式,然后是一对大括号 {} 之间的若干条语句。如果表示条件的逻辑表达式的结果不为 0,那么就执行大括号中的语句;否则,跳过这些语句不执行,而继续下面的其他语句。

注意 if 语句中,if 关键字所在的那一行是没有 ; 的,而下面的语句缩进并有 ;。这表明这条语句是 if 语句的一部分,if 语句拥有和控制这条语句,决定它是否要执行。

Tip

if...else 语句可以不写大括号,但 ifelse 只有其所在行的下一行语句的控制权,即只能运行单行代码。例如:

1
2
3
if (a > b)
max = a;
printf("最大值是a");

即使不满足 a > bprintf 语句仍然会执行,因为它不受 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
    16
    const 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
2
3
4
5
6
7
8
9
10
11
12
13
14
const int STANDARD = 60;
int score = 0;
printf("请输入你的成绩:");
scanf("%d", &score);
printf("你输入的成绩为:%d", score);
if (score >= 60)
{
printf("恭喜你,及格了!");
}
else
{
printf("很遗憾,没有及格。");
}
printf("再见。\n");

7.6 我的面前有好多条路 ——else if 语句

  • 问题描述:请你写一个程序,输入一个 4 位及以下的正整数,然后输出这个整数的位数。

    先从算法角度思考这个问题:

    • 对于人来说,这件事情就是眼睛盯一下就可以得出结果的事。但对于计算机而言,天哪,那是不可能的.jpg
    • 计算机用的一种方式是通过判断数的范围来作为它的位数。例如,352∈[100,999],则 352 是一个三位数。

    如果我们通过范围判断位数,那么我们起码要准备三个分支(还有一个分支可以用假设法优化掉)。可 if...else... 语句只支持两个分支,怎么办?

    你可能会觉得这只是 “多写几个 if 语句” 的事,但几个 if 语句间是互相独立的,它们的判断也是独立的,最终结果由最后的那个 if 决定。也就是说,你可能试了几个不同位数的数,但程序的惊天智慧会认为它们的位数是一样的。

    为了应对一个判断、更多分支的情况,我们要使用 else if... 语句。

    else ifif 一样需要条件,使用起来也和 if 差不多。与几个 if 语句并排不同的是,只要其中一个分支符合条件,那么接下来的其他分支便不再进行判断

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    int 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
2
3
4
5
6
7
8
9
int x;
int n = 0;
scanf("%d", &x);
while (x > 0)
{
n++;
x /= 10;
}
printf("%d\n", n);

输入 x 后,while 函数会先判断是否满足 x > 0,满足后执行 {} 内的代码,执行完后再次判断条件,满足后再次执行 {} 内的代码,然后再次判断条件。条件不满足时,停止循环,执行下面的 printf

注意循环体内要有改变条件的机会,否则循环会一直进行下去,变成死循环,你的程序会无法自动退出!


8.2 while 循环 —— 条件优先,循环次数不定

while 循环和 if 判断在语法上很相似,只是换了一个关键字,但它们在原理上是不一样的。while 循环的意思是:当条件满足时,不断地重复循环体内的语句。

在循环执行之前需要判断是否满足条件,因此有可能循环一次也没有被执行条件成立是循环继续的前提。

while 循环的基本模式是这样的:


8.3 do-while 循环 —— 循环体优先,循环次数不定

do-while 循环在首次进入循环时不做检查,而是在执行完一轮循环体的代码后,再检查循环的条件是否满足,如果满足则继续下一轮循环,不满足则结束循环。

while 循环不一样的是,do-while 循环至少执行一次,然后再判断条件。

do-while 循环的语法是这样的:

1
2
3
4
5
do
{
//循环体语句
}
while (<循环条件>);

基本模式是这样的:


8.4 for 循环 —— 条件优先,循环次数一定

  • 问题描述:请你写一个程序,输入 nn,输出 n!n!。其中 n!n! 为 n 的阶乘,n!=1×2×3×...×nn! = 1 × 2 × 3 × ... × n

数据角度考虑:

  • 读取用户输入需要一个 int 类型的 n,然后计算的结果也需要一个变量存储,如 int 类型的 factor,在计算中还需要一个变量从 1 递增到 n,可以是 int 类型的 i

计算角度考虑:

  • 我们可以使用 while 循环来处理这个问题:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    int 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
    4
    for (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
2
3
4
5
6
int n, factor = 1;
scanf("%d", &n);
for (n > 1; n--)
{
factor *= n
}

for 循环的语法是这样的:

1
2
3
4
5
for (<初始化计数器>; <条件>; <改变计数器>)
//三个表达式,每个都可以省略
{
//循环体
}

计数器变量可以在 for 语句外定义,也可以在 for 语句内定义。不同的定义位置会影响计数器的作用域。简单来讲,for 语句内定义的计数器不可用于循环体外,for 语句外定义的计数器在循环体内外都可以使用。

注意循环次数。对于 for (i = 0; i < n; i++) 来说,它的循环次数为 n,而循环结束后,i 的值是 n。循环控制变量是初始化为 0 还是 1、条件是 i < n 还是 i <= n,对循环次数、循环结束后变量的值都有影响。

基本模式是这样的:

实际上,for 循环可以视为 while 循环的一种变体。例如,以下两段代码等价。

1
2
3
4
for (int i = 1; i <= n; i++)
{
factor *= i;
}
1
2
3
4
5
6
int i = 1;
while (i <= n)
{
factor *= i;
i++;
}

Lesson 9 逻辑类型和运算

9.1 逻辑类型

逻辑类型也叫布尔(bool)类型,也是一个数据类型。布尔类型只有两个值:truefalse。但在 C 语言中,bool 类型的数据只能当作 int 类型来输入输出,也就是说,不能直接输入输出 truefalse,而只能输入输出非 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
2
3
a = cond ? 1+2 : 1*2
// if cond = true, a = 1+2 = 3
// if cond = false, a = 1*2 = 2

条件运算符的优先级比赋值运算符的高,但比其他运算符的低。

条件运算符是从右到左结合的。

不建议在条件运算符中过分杂糅其他运算符,也不建议将几个条件运算符嵌套书写。


9.4 逗号运算

在 C 语言中,逗号用来连接两个表达式,并以其右边的表达式的结果作为它的结果。

逗号的优先级是所有表达式中最低的,所以它两边的表达式会先计算。

逗号的结合关系是从左到右,所以左边的表达式会先计算,而右边的表达式的值就作为逗号运算的结果。

逗号运算可以用在 for 语句中,用来处理多个计数器的情况。

1
2
3
4
for (i = 0, j = 10; i < j; i++, j--)
{
...
}

  1. 两个 =,注意不要和赋值运算符 = 混淆 ↩︎

  2. !=,中间不留空,顺序不能颠倒 ↩︎

  3. >=,中间不留空,顺序不能颠倒 ↩︎

  4. <=,中间不留空,顺序不能颠倒 ↩︎

  5. ⚠️这里是单目的 + 和 - ↩︎


©2025-Present Watermelonabc | 萌ICP备20251229号

Powered by Hexo & Stellar latest & Vercel & 𝙌𝙞𝙪𝙙𝙪𝙣 𝘾𝘿𝙉 & HUAWEI Cloud
您的访问数据将由 Vercel 和自托管的 Umami 进行隐私优先分析,以优化未来的访问体验

本博客总访问量:capoo-2

| 开往-友链接力 | 异次元之旅 | 中文独立博客列表

猫猫🐱 发表了 41 篇文章 · 总计 209.8k 字