8.1 数值专题:C 程序中的整数与浮点数
408 数值类综合题的解题方法——机器数与真值、同一加法器上的有符号/无符号运算与标志位、C 语言类型转换(符号扩展、零扩展、截断、整数与浮点互转)、浮点数的精度与表示范围。
408 怎么考数值
数值部分的综合题几乎都以一段 C 程序为载体,题目给出变量的类型和初值,然后追问:
| 常见问法 | 用到的知识 |
|---|---|
| 某寄存器的内容是什么(十六进制)? | 补码 / 无符号数的机器数,模 运算 |
| 某变量的值是多少(十进制)? | 同一机器数按不同类型解释 |
| 能否用同一个加法器实现?标志位是多少? | 加法器与 OF、SF、ZF、CF 的生成 |
| 哪些语句的结果溢出?如何判断? | 溢出判断的三种方法 |
| 两个函数结果为什么不同?最大的 n 是多少? | 整数溢出、浮点数的有效位数与表示范围 |
这些问题的答案都来自一个基本事实:CPU 中只有二进制位串,“类型”只存在于编译器选择的指令和我们对结果的解释中。
- 写出机器数:把每个变量的初值按其类型写成 位机器数(带符号整数用补码)。
- 在位模式上运算:加、减运算一律是 位二进制加法(减法是加上减数取反加 1),结果只保留低 位,即模 。
- 按类型解释结果:无符号数直接按权展开;带符号数最高位权值为 ;需要时再判断溢出。
同一串位,不同的解释
| 机器数 | 无符号整数 | 补码整数 | 当作 float |
|---|---|---|---|
8 位 86H | 134 | −128 + 6 = −122 | — |
8 位 F6H | 246 | −10 | — |
16 位 FFFAH | 65530 | −6 | — |
32 位 FFFFFFFFH | 4 294 967 295 | −1 | NaN |
32 位 80000000H | 2³¹ | −2³¹(int 最小值) | −0.0 |
32 位 3F800000H | 1 065 353 216 | 1 065 353 216 | 1.0 |
C 语言的类型转换
赋值、传参、混合运算时,C 编译器会按下表改变机器数:
| 转换 | 机器数的变化 | 真值 |
|---|---|---|
位长相同,有符号 ↔ 无符号(如 int ↔ unsigned) | 不变 | 按新类型重新解释,可能改变 |
短 → 长,源为带符号类型(如 short → int) | 符号扩展:高位全部填符号位 | 不变 |
短 → 长,源为无符号类型(如 unsigned short → int) | 零扩展:高位全部填 0 | 不变 |
长 → 短(如 int → short) | 截断:直接丢掉高位 | 可能改变 |
int → float | 重新编码为 IEEE 754 | 不溢出,但有效位超过 24 位时舍入 |
int/float → double | 重新编码 | 精确 |
float/double → int | 重新编码,小数部分向 0 截断 | 超出范围时为未定义行为 |
double → float | 重新编码 | 可能舍入,也可能上溢为 ∞ |
用下面的实验观察各种转换中位模式的变化。选择”典型例子”可以快速切换到常考情形;输入以 0x 开头的十六进制数时,按机器数理解。
加减运算、溢出与标志位
无符号整数和带符号整数的加减法在同一个加法器上完成:加法直接相加,减法把减数各位取反、并令最低位进位 。加法器同时输出四个标志,至于用哪个标志判断结果,由程序中变量的类型决定:
| 标志 | 生成方式 | 含义 |
|---|---|---|
| OF(溢出) | (最高位进位与次高位进位异或) | 带符号运算是否溢出 |
| SF(符号) | 结果的最高位 | 带符号结果的正负 |
| ZF(零) | 结果各位全 0 时为 1 | 结果是否为 0 |
| CF(进/借位) | 无符号运算是否溢出(加法有进位 / 减法有借位) |
带符号运算溢出的三种判断方法:① 两个同号数相加,结果符号与之相反;② 双符号位(变形补码)结果为 01(正溢)或 10(负溢);③ 。硬件中常用第 ③ 种。详见第 2 章「定点数的运算」与「加法器与运算器」。
假定在一个 8 位字长的计算机中运行如下类 C 程序段,编译器将 8 个 8 位寄存器 R1~R8 依次分配给变量 x、y、m、n、z1、z2、k1、k2(带符号整数用补码表示):
unsigned int x = 134;
unsigned int y = 246;
int m = x;
int n = y;
unsigned int z1 = x - y;
unsigned int z2 = x + y;
int k1 = m - n;
int k2 = m + n;(1) 执行后 R1、R5、R6 的内容分别是什么(十六进制)?(2) 变量 m 和 k1 的值分别是多少(十进制)?(3) 这四种运算能否利用同一个加法器实现?(4) 计算机内部如何判断带符号整数加减运算是否溢出?哪些语句的结果会溢出?
查看解答
第一步:写机器数。 x = 134 = 1000 0110B = 86H,y = 246 = 1111 0110B = F6H。int m = x 是同位长的有符号/无符号转换,机器数不变:m = 86H,n = F6H。
第二步:在位模式上运算(模 2⁸)。
| 变量 | 计算 | 机器数 | 按类型解释 |
|---|---|---|---|
| z1 = x − y | 86H + 0AH(F6H 取反加 1) = 90H | 90H | 无符号 144(真实差为 −112,结果错误) |
| z2 = x + y | 86H + F6H = 1 7CH,丢弃进位 | 7CH | 无符号 124(真实和 380 > 255,溢出) |
| k1 = m − n | 与 z1 相同的位运算 | 90H | 补码 −112 = (−122) − (−10),正确 |
| k2 = m + n | 与 z2 相同的位运算 | 7CH | 补码 124,而 (−122) + (−10) = −132 < −128,溢出 |
(1) R1 = x = 86H,R5 = z1 = 90H,R6 = z2 = 7CH。
(2) m 的机器数 86H 按补码解释为 −128 + 6 = −122;k1 的机器数 90H 为 −128 + 16 = −112。
(3) 能。无符号数与补码的加减运算在位模式上完全相同(减法都是”加上减数的反码再加 1”),区别只在于对结果的解释以及判断溢出的方法(无符号看 CF,带符号看 OF),因此可以共用一个加法器,再配上不同的溢出判断电路。
(4) 用最高数值位的进位 与符号位的进位 的异或判断:,为 1 表示溢出。带符号运算只有 k1、k2 两条:k1 为 −122 − (−10) 不溢出;k2 = 86H + F6H:两个负数相加得到正数 7CH(,,OF = 1),k2 溢出。
浮点数的精度与范围
IEEE 754 单精度 float 有 1 位符号、8 位阶码(偏置 127)、23 位尾数,加上隐含的最高位 1,共有 24 位有效位。由此得到两个重要结论:
- 精度:绝对值不超过 的整数都能被 float 精确表示; 是第一个不能精确表示的正整数。因此
int→float可能丢精度,而int→double(53 位有效位)总是精确的。 - 范围:float 的最大值为 ,远大于 int 的最大值 。所以同一个计算,int 版本先溢出(结果错得离谱),float 版本先舍入(结果有微小误差),直到超过 才上溢为 ∞。
| 阶码 | 尾数 | 表示 |
|---|---|---|
| 全 0 | 全 0 | ±0 |
| 全 0 | 非 0 | 非规格化数 |
| 1~254 | 任意 | 规格化数 |
| 全 1 | 全 0 | ±∞(如 7F800000H 为 +∞) |
| 全 1 | 非 0 | NaN |
下面的实验对应一道经典真题:函数 f1、f2 计算 ,区别只是变量类型。拖动 n,或点击彩色条带中的任意位置,观察两个函数在何处开始出错:
int f1(unsigned n) { /* f2:把 int 换成 float */
int sum = 1, power = 1;
for (unsigned i = 0; i <= n - 1; i++) {
power *= 2;
sum += power;
}
return sum;
}| 返回值 | 机器数 | 状态 | |
|---|---|---|---|
| 精确值 f(24) | 33,554,431 | 2²⁵ − 1 | |
| f1(24) | 33,554,431 | 01FFFFFFH | 结果精确 |
| f2(24) | 33,554,432 | 4C000000H | 有舍入误差 |
针对上面的 f1 与 f2(unsigned 与 int 都是 32 位,float 为 IEEE 754 单精度):(1) n = 0 时 f1 为何死循环?若 i、n 都改为 int 型,还会死循环吗?(2) f1(23) 与 f2(23) 的返回值是否相等?机器数各是什么?(3) f1(24) 与 f2(24) 的返回值分别为 33 554 431 和 33 554 432.0,为什么不相等?(4) f(31) = 2³² − 1,而 f1(31) 的返回值却为 −1,为什么?使 f1(n) 返回值与 f(n) 相等的最大 n 是多少?(5) f2(127) 的机器数为 7F800000H,对应的值是什么?使 f2(n) 结果不溢出的最大 n 是多少?结果精确(无舍入)的最大 n 是多少?
查看解答
(1) n 是 unsigned,n − 1 = 0 − 1 按模 2³² 运算得 FFFFFFFFH = 2³² − 1,是 unsigned 能表示的最大值,i <= n - 1 恒成立,所以死循环。改为 int 后,n − 1 = −1,0 <= -1 不成立,不会死循环。
(2) 相等。f(23) = 2²⁴ − 1,即 24 个 1。f1(23) 的机器数为 00FFFFFFH。f2(23) = 1.111…1(小数点后 23 个 1)× 2²³,符号 0,阶码 23 + 127 = 150 = 1001 0110B,尾数为 23 个 1:0 10010110 111…1 = 4B7FFFFFH。24 位有效位恰好放得下,没有舍入。
(3) f(24) = 2²⁵ − 1 有 25 位有效位,而 float 只有 24 位,需要舍入。尾数全为 1,舍入时产生连锁进位,得到 ,比精确值大 1。f1(24) 是 int 运算,精确。
(4) 2³² − 1 超出了 int 的范围 ,机器数为 FFFFFFFFH,按补码解释为 −1。f1(n) = 2^(n+1) − 1 ≤ 2³¹ − 1 要求 n + 1 ≤ 31,最大 n = 30。
(5) 7F800000H:阶码全 1、尾数全 0,是 +∞。float 的最大值 ;由 得 最大 n = 126。由 (2)(3) 可知结果精确的最大 n = 23。
规律:两个计算方法相同的函数,若结果相差很大,通常是表示范围小的类型发生了溢出;若结果只差一点,通常是浮点数的舍入误差。
非 IEEE 格式的浮点运算
有些题目会自定义浮点格式(阶码、尾数都用补码),并要求按”对阶 → 尾数运算 → 规格化 → 舍入 → 判溢出”五步计算。补码尾数的规格化形式是 符号位与最高数值位不同(正数 0.1×…,负数 1.0×…)。
设 12 位浮点数格式为:阶码 4 位(含 1 位阶符),尾数 8 位(含 1 位数符),阶码和尾数均用补码表示,底为 2,阶码在前。已知 X = −11.25,Y = −7.875。(1) 写出 X、Y 的规格化浮点数表示;(2) 浮点机器数 64CH 的真值是多少?(3) 计算 X + Y(0 舍 1 入)。
查看解答
(1) X = −11.25 = −1011.01B = −0.1011010 × 2⁴:阶码 4 = 0100,尾数 [−0.1011010]补 = 1.0100110,[X]浮 = 0100 1.0100110(= 4A6H)。
Y = −7.875 = −111.111B = −0.1111110 × 2³:阶码 0011,尾数 [−0.1111110]补 = 1.0000010,[Y]浮 = 0011 1.0000010(= 382H)。
(2) 64CH = 0110 0100 1100B,阶码 0110 = 6,尾数 0.1001100 = 0.59375,真值 = 0.59375 × 2⁶ = 38。
(3) 用双符号位计算。
- 对阶:ΔE = 4 − 3 = 1,小阶向大阶看齐,Y 的尾数算术右移 1 位:
11.0000010→11.10000010(多出的一位暂时保留,用于舍入),阶码变为 4。 - 尾数相加:
11.01001100+11.10000010=10.11001110(丢弃最高位进位)。 - 规格化:双符号位为
10,尾数溢出,需要右规:尾数算术右移 1 位得11.01100111|0,阶码加 1 变为 5。 - 舍入:保留 7 位数值位
11.0110011,被移出的最高位为 1,按 0 舍 1 入末位加 1,得11.0110100。 - 判溢出:阶码 5 =
0101在 4 位补码范围 −8~7 内,不溢出。
结果 [X + Y]浮 = 0101 1.0110100,即 −0.1001100 × 2⁵ = −19(精确值为 −19.125,误差来自舍入)。
即时练习
- 1执行 C 语句
short si = -32767; unsigned short usi = si;后,usi 的值是( )。 - 2某 32 位机器上 int 型变量 x 的机器数为 FFFF FFDFH,则 (unsigned) x 和 x 的值分别是( )。
- 3下列 32 位 int 型整数中,不能用 IEEE 754 单精度 float 精确表示的是( )。
- 4在 32 位机器上执行
int a = -1; unsigned b = 1;,则表达式a < b的值以及原因是( )。 - 5float 型变量 x 的机器数为 C0A0 0000H,则 x 的值是( )。
- 68 位加法器计算 86H + F6H 后,标志 CF、OF、SF、ZF 依次为( )。
标记完成后,侧栏会显示进度。进度仅保存在本浏览器中。