表达式求值: 优先级与结合性
求值顺序、副作用与左值
类型转换: 隐式与强制
演示与任务 4: %f 与转换实验
编码实践: 四舍五入、数字拆分、时间换算
课堂习题与小测
| 优先级 | 名称 | 符号 | 结合性 |
|---|---|---|---|
| 1 | 自增自减(后缀) | ++ -- |
left |
| 2 | 自增自减(前缀)、正负号、按位取反 | ++ -- + - ~ |
right |
| 3 | 乘法类 | * / % |
left |
| 4 | 加法类 | + - |
left |
| 5 | 移位 | << >> |
left |
| 6 | 按位与 | & |
left |
| 7 | 按位异或 | ^ |
left |
| 8 | 按位或 | | |
left |
| 9 | 赋值类 | = += -= *= /= %= <<= >>= &= ^= |= |
right |
单目 (1、2 级) 高于双目算术; 移位 (5 级) 与按位与/异或/或 (6-8 级) 都在算术之下; 赋值 (9 级) 最低
后缀 ++/-- 左结合; 前缀、一元 +/-/~、赋值右结合
后缀紧贴操作数: -x++ 是 -(x++); 若反过来, (-x)++ 对非左值自增, 直接报错
给缺少括号的表达式按优先级逐层加括号 (右例); 拿不准就加括号, 永远不会错
a = b += c++ - d + --e / -f
a = b += (c++) - d + --e / -f
a = b += (c++) - d + (--e) / (-f)
a = b += (c++) - d + ((--e) / (-f))
a = b += (((c++) - d) + ((--e) / (-f)))
(a = (b += (((c++) - d) + ((--e) / (-f)))))
表达式的值可能取决于子表达式的计算顺序——C 没有定义这个先后 (逻辑与或、条件、逗号运算符除外)
a = 5;
c = (b = a + 2) - (a = 1); /* c 是几? */
i = 2;
j = i * i++; /* j 是 4 还是 6? */
同一表达式里既读又改同一变量, 产生未定义行为: 结果随编译器、优化级别不同
后果可能是: 结果不同 / 崩溃 / "看起来正常"——第三种最危险, 换个环境就可能出错
规避: 拆成多条语句, 每句只做一件事; b = a + 2; a = 1; c = b - a; 中 c 确定是 6
副作用: 修改操作数的运算符具有副作用; i + j 没有副作用, 而赋值、自增自减有
表达式 i = 0 的结果是 0, 副作用是把 0 存进 i; 嵌入式赋值语法合法但难读, 不推荐
i = 1;
k = 1 + (j = i); /* k = 2, i = j = 1 */
左值: 赋值运算符需要一个左值作为左操作数
左值表示存储在计算机内存中的对象, 不是常量或计算结果; 变量是左值, 10、2 * i 不是
12 = i;、i + j = 0;、-i = j; 都非法, 编译器报 "invalid lvalue in assignment"
隐式转换: 不同类型混合运算/赋值时, 编译器自动转换
赋值: 右值自动转换为左值变量的类型
运算: 较低类型提升为较高类型 (char/short → int; int + double → double)
int i;
double d = 3.14159;
i = d; /* i 为 3 (截断小数) */
d = i; /* d 为 3.0 */
1 / 2; /* 0 (整数除法) */
1.0 / 2; /* 0.5 (2 提升为 double) */
强制转换: 显式地把值转换为目标类型
(float) 1 / 2; /* 0.5 */
(int) 3.9; /* 3 */
强制转换的优先级很高, 只作用于紧邻的操作数; 需要转换整个表达式时要加括号, 如 (int) (3.9 + 1.1)
强制转换还能避免溢出: 先提升, 再运算
long i; int j = 1000000;
i = (long) j * j; /* 正确: j 先转成 long */
i = (long) (j * j); /* 错误: j * j 已在 int 里溢出 */
隐式转换由编译器自动完成, 强制转换由程序员显式写出; 两者都按 C 的类型转换规则执行
看这段代码, 预测输出——尤其是第二行:
int a = 9, b = 4;
printf("%d\n", a / b); /* 整数除法 */
printf("%f\n", a / b); /* %f 却收到 int */
第二行会打出什么? 猜一猜
int a = 9, b = 4;
printf("%d\n", a / b); /* 2: 整数除法截断 */
printf("%f\n", a / b); /* 错! 用 %f 打印 int → 结果不可预测 */
printf("%f\n", (double)a / b); /* 2.250000: 先转换再除 */
printf("%f\n", a / 4.0); /* 2.250000: 4.0 是 double, 整体提升 */
常见的运行结果:
2
0.000000
2.250000
2.250000
类型转换发生在运算时: a / b 两个 int 相除, 先截断、后转换, 轮到 printf 时已经来不及了
%f 期待 double 却收到 int → 未定义行为, 常见打出 0.000000 或乱码——printf 不会替你检查类型
正确姿势: 转换一个操作数 (double)a / b, 或直接用浮点字面量 a / 4.0
任务: 观察并解释四组输出:
7 / 2 与 7 / 2.0
'A' 当整数打印
(unsigned)-1 用 %u 打印
(int)3.99 与 (int)-3.99
样例 (常见结果):
7 / 2 = 3, 7 / 2.0 = 3.5
'A' 的整数值 = 65
(int)3.99 = 3, (int)-3.99 = -3
提示: 混合类型先转换; 强制转换向零截断, 不是四舍五入
⚡ 在 VS Code 中打开 task-convert.c (骨架)#include <stdio.h>
int main(void)
{
printf("7 / 2 = %d, 7 / 2.0 = %f\n", 7 / 2, 7 / 2.0);
printf("'A' 的整数值 = %d\n", 'A');
printf("(unsigned)-1 = %u\n", (unsigned)-1);
printf("(int)3.99 = %d, (int)-3.99 = %d\n", (int)3.99, (int)-3.99);
return 0;
}
7/2 是整数除法得 3; 有一个操作数是 double 时, 另一个先隐式转换, 才得 3.5
char → int 得到编码 65——0-4 的 ASCII 知识
负数转 unsigned: 按 2 的幂取模, -1 变成 4294967295
强制转换 (int) 向零截断: 3.99→3, -3.99→-3; 要实现四舍五入需另加修正
输入一个正实数 x, 输出它四舍五入后的整数
例:
输入: 3.7 → 输出: 4
输入: 3.4 → 输出: 3
输入: 2.5 → 输出: 3
注意: printf("%.0f") 只是打印时四舍五入, 变量本身还是 double; 本题要求得到整数变量的值
提示: 强制转换是"向零截断"——小数部分直接丢掉; 先 +0.5 再截断
负数怎么处理, 等学了 if 再补 (现在只要求正数)
#include <stdio.h>
int main(void) {
double x;
printf("请输入一个数: ");
scanf("%lf", &x);
int r = (int)(x + 0.5);
printf("%.2f 四舍五入 = %d\n", x, r);
return 0;
}
核心一行: (int)(x + 0.5)——先加 0.5 再截断, 3.7 → 4、3.4 → 3
运行示例: 输入 3.7 → 3.70 四舍五入 = 4
试一试负数: (int)(-3.7 + 0.5) = -3, 但 -3.7 四舍五入应为 -4——C 的截断是向零取整, 负数要用 (int)(x - 0.5)
一个程序同时处理正负需要条件判断——学完 if 就能写出来
输入一个4 位整数 N (1000~9999), 输出两样东西:
各位数字之和
反转后的数
例: 输入 1234 → 输出 和 = 10、反转 = 4321
本题假设输入合法 (就是 4 位数); 想拒绝非 4 位数的输入, 要等学了 if 之后才能做校验
提示: / 取高位、% 取低位
#include <stdio.h>
int main(void) {
int n;
scanf("%d", &n);
int qian = n / 1000;
int bai = n / 100 % 10;
int shi = n / 10 % 10;
int ge = n % 10;
printf("和 = %d\n", qian + bai + shi + ge);
printf("反转 = %d%d%d%d\n", ge, shi, bai, qian);
return 0;
}
千位 n / 1000; 百位 n / 100 % 10; 十位 n / 10 % 10; 个位 n % 10——先除后模, 一层层剥
反转数两种做法: %d%d%d%d 直接排四个一位数 (注意 1000 会打成 0001), 或 ge*1000 + shi*100 + bai*10 + qian 重新组装
输入 1234 → 和 = 10、反转 = 4321
输入总秒数 N, 输出"X 小时 Y 分 Z 秒"
例: 输入 3671 → 输出 1 小时 1 分 11 秒
提示: 一小时 3600 秒
#include <stdio.h>
int main(void) {
int N;
printf("请输入总秒数: ");
scanf("%d", &N);
int h = N / 3600;
int m = N % 3600 / 60;
int s = N % 60;
printf("%d 小时 %d 分 %d 秒\n", h, m, s);
return 0;
}
h = N / 3600: 一共几小时; m = N % 3600 / 60: 扣掉小时剩下的秒里取分钟; s = N % 60: 最后剩下的秒
层层剥: 先除后模, 与数字拆分的千/百/十/个位方法一致
输入 3671 → 1 小时 1 分 11 秒
三道题, 限时 10 分钟, 每题都藏着一个坑:
C, 输出华氏度 F = C × 9 / 5 + 32。检验: 100 应输出 212; 若输出 132, 说明踩了整数除法的坑
86 92 78, 输出 85.33 (注意: 三个数加起来不能被 3 整除, 直接 (a+b+c)/3 会悄悄丢小数!)
int i = (int) 3.9; 中 i 的值是多少?(float) 1 / 2 的值是多少?
int i = 3.99; 初始化后 i 是多少?编译器默认会警告精度损失吗?
j = i * i++; 的结果不确定?这类表达式应该怎么避免?