Lesson 5 浮点数

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

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

    换算关系为:(英尺数 + 英寸数 ÷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 1.33.1 & Vercel & HUAWEI Cloud
您的访问数据将由 Vercel 和自托管的 Umami 进行隐私优先分析,以优化未来的访问体验

本博客总访问量:capoo-2

| 开往-友链接力 | 异次元之旅

中文独立博客列表 | 博客录 随机博客

AI 参与指数(IIIA)2 级

猫猫🐱 发表了 56 篇文章 · 总计 232.1k 字