智能程序设计(C语言)


类型转换与未定义行为

计算机学院    杨已彪

yangyibiao@nju.edu.cn

提纲

  • 表达式求值

  • 副作用与左值

  • 类型转换

  • 编码实践


表达式求值

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

优先级 名称 符号 结合性
1 自增(后缀) ++ left
自减(后缀) --
2 自增(前缀) ++ right
自减(前缀) --
一元正号 +
一元负号 -
3 乘法类 * / % left
4 加法类 + - left
5 赋值 = *= /= %= += -= right
  • 单目运算符 (1、2 级) 高于双目算术, 赋值 (5 级) 最低

  • 结合性: 后缀 ++/-- 左结合; 前缀 ++/--、一元 +/-、赋值是右结合; 算术左结合

优先级表可用于给缺少括号的表达式加括号: 从最高优先级开始, 在运算符及其操作数周围加上括号 (下一页演示)


表达式求值

例子:

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 + b) * (c - d): 先算哪个括号? 不确定; 大多数表达式结果相同, 但子表达式修改操作数时就不成立:

a = 5;
c = (b = a + 2) - (a = 1);
  • 第二条语句的执行结果是未定义的

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


子表达式求值顺序

为防止出现问题, 最好避免在子表达式中使用赋值运算符

相反, 建议使用一串分离的赋值表达式, 这样 c的值是确定的, 将始终为 6

除了赋值运算符之外, 唯一修改其操作数的运算符是自增和自减

很自然地认为j被赋值为4. 然而, j也可以被赋值为6——求值顺序不同, 结果不同

a = 5;
b = a + 2;
a = 1;
c = b - a;
i = 2;
j = i * i++;

未定义的行为

c = (b = a + 2) - (a = 1);和j = i * i++;均会产生未定义的行为

未定义行为的可能影响:

  • 当使用不同的编译器编译时, 程序运行的行为可能会有所不同

  • 该程序可能一开始就无法编译

  • 能编译但无法运行

  • 能运行, 但程序可能会崩溃、行为不正常或产生无意义的结果

应避免未定义的行为


副作用(side effects)

修改操作数的运算符被称为具有 副作用 , 如 i + j 没有 副作用

  • 简单的赋值运算符有副作用: 它修改了左操作数, 表达式 i = 0 的结果为 0, 它的副作用是将 0 赋值给i

  • 允许 v 类型值的地方都可以进行形如 v = e 的赋值:

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

--


编码实践: mol

\(6\) 克氧气的分子数是多少?

\(Q = 6 / 32 \times 6.02 \times 10^{23}\)

两种格式输出, 结果均用科学计数法表示:

  • 第一行: 小数点后保留 \(3\) 位 → %.3e

  • 第二行: 保留 \(5\) 位有效数字 → %.5g

提醒: 6 和 32 都是整数——直接写 6 / 32 会发生什么? (上节课 9/4 的坑!) 先自己写, 再翻页看参考代码


编码实践: mol (参考代码)

/*
 * mol.c(6克氧气的分子数)
 */
#include <stdio.h>

int main(void) {
    const double MOL = 6.02E23;
    const int GRAM_PER_MOL = 32;
    int mass = 6;

    double quantity = mass * 1.0 /GRAM_PER_MOL * MOL;

    printf("quantity = %.3e\nquantity = %.5g\n", quantity, quantity);
    return 0;
}
  • mass * 1.0 / GRAM_PER_MOL: 乘上 1.0 把运算提升为浮点, 避开整数除法的坑

  • %.3e 与 %.5g 分别控制小数位与有效数字

  • 运行结果: quantity = 1.129e+23 与 1.1288e+23


课堂小测

  1. int i = (int) 3.9; 中 i 的值是多少?(float) 1 / 2 的值是多少?
  2. i = d; 中 d 是 double 型 3.99,赋值后 i 是多少?有精度损失提示吗?
  3. 为什么 j = i * i++; 的结果不确定?这类表达式应该怎么避免?