智能程序设计(C语言)


类型转换与未定义行为

计算机学院    杨已彪

yangyibiao@nju.edu.cn

提纲

  • 表达式求值

  • 副作用与左值

  • 类型转换

  • 编码实践

  • 演示、习题与小测


表达式求值

到目前为止所涉及的运算符:

优先级 名称 符号 结合性
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 1
a = b += (c++) - d + (--e) / (-f) 2
a = b += (c++) - d + ((--e) / (-f)) 3
a = b += (((c++) - d) + ((--e) / (-f))) 4
(a = (b += (((c++) - d) + ((--e) / (-f))))) 5
  • 顺序: 后缀 ++ → 前缀/一元 → 乘除 → 加减 → 赋值 (右结合, 最后整体加)


子表达式求值顺序与未定义行为

表达式的值可能取决于子表达式的计算顺序——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 */

避免在表达式中访问变量值的同时也修改该变量


副作用(side effects)

修改操作数的运算符叫作具有 副作用; i + j 没有副作用, 而 赋值、自增自减有

  • 表达式 i = 0 的结果是 0, 副作用是把 0 存进 i

i = 1;
k = 1 + (j = i);
printf("%d %d %d\n", i, j, k);
/* prints "1 1 2" */
  • 这种嵌入式赋值语法合法, 但难读、易藏错——不推荐


左值

赋值运算符需要一个 左值 作为其左操作数

左值 表示存储在计算机内存中的对象, 而不是常量或计算结果

  • 变量是左值

  • 10或2 * i这样的表达式不是左值

12 = i;      /*** WRONG ***/
i + j = 0;   /*** WRONG ***/
-i = j;      /*** WRONG ***/

编译器将产生一条错误消息, 例如"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) */

演示: 为什么 %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


类型转换 - 强制转换

强制转换: 显式地把值转换为目标类型

(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 里溢出了 */

动手小练习: 四舍五入

printf 打印 double 会自动四舍五入, 但怎样把 double 变量本身四舍五入成整数?

提示: 强制转换是"向零截断"——小数部分直接丢掉


动手小练习: 四舍五入 (参考代码)

#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)

  • 一个程序同时处理正负需要条件判断——第 4 周学完 if 就能写出来


编码实践: 数字拆分

输入一个4 位整数 N (1000~9999), 输出两样东西:

  • 各位数字之和

  • 反转后的数

例: 输入 1234 → 输出 和 = 10、反转 = 4321

本题假设输入合法 (就是 4 位数); 想拒绝非 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 分钟, 每题都藏着一个坑 (答案见讲义):

  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++; 的结果不确定?这类表达式应该怎么避免?