智能程序设计(C语言)


类型转换与未定义行为

计算机学院    杨已彪

yangyibiao@nju.edu.cn

提纲

  • 表达式求值: 优先级与结合性

  • 求值顺序、副作用与左值

  • 类型转换: 隐式与强制

  • 演示与任务 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 的类型转换规则执行


演示: 为什么 %f 打出了奇怪的数?

看这段代码, 预测输出——尤其是第二行:

int a = 9, b = 4;
printf("%d\n", a / b);          /* 整数除法 */
printf("%f\n", a / b);          /* %f 却收到 int */

第二行会打出什么? 猜一猜


演示: 为什么 %f 打出了奇怪的数? (实际结果)

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


任务 4: 隐式转换与强制转换实验

任务: 观察并解释四组输出:

  1. 7 / 2 与 7 / 2.0
  2. 'A' 当整数打印
  3. (unsigned)-1 用 %u 打印
  4. (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 (骨架)

任务 4: 参考代码与要点

#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 再补 (现在只要求正数)

⚡ 在 VS Code 中打开 task-round.c (骨架)
动手小练习: 四舍五入 (参考代码)

代码
#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 之后才能做校验

提示: / 取高位、% 取低位

⚡ 在 VS Code 中打开 task-digits.c (骨架)
编码实践: 数字拆分 (参考代码)

代码
#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 秒

⚡ 在 VS Code 中打开 task-time.c (骨架)
编码实践: 时间换算 (参考代码)

代码
#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 分钟, 每题都藏着一个坑:

  1. 温度转换: 输入摄氏度 C, 输出华氏度 F = C × 9 / 5 + 32。检验: 100 应输出 212; 若输出 132, 说明踩了整数除法的坑
  2. 平均分: 输入三门课成绩 (整数), 输出平均分保留两位小数。例: 输入 86 92 78, 输出 85.33 (注意: 三个数加起来不能被 3 整除, 直接 (a+b+c)/3 会悄悄丢小数!)
  3. (选做) XOR 交换: 用位运算交换两个变量的值, 不用临时变量; 试试两个变量相同会怎样

课堂小测

  1. int i = (int) 3.9; 中 i 的值是多少?(float) 1 / 2 的值是多少?
  2. int i = 3.99; 初始化后 i 是多少?编译器默认会警告精度损失吗?
  3. 为什么 j = i * i++; 的结果不确定?这类表达式应该怎么避免?



未完待续