8.1 数值专题:C 程序中的整数与浮点数

408 数值类综合题的解题方法——机器数与真值、同一加法器上的有符号/无符号运算与标志位、C 语言类型转换(符号扩展、零扩展、截断、整数与浮点互转)、浮点数的精度与表示范围。

考查要求 ★★★ 重点掌握 先写机器数再解释无符号与补码共用加法器OF/CF/SF/ZF符号扩展与零扩展截断int 与 float 互转的精度IEEE 754 特殊值

408 怎么考数值

数值部分的综合题几乎都以一段 C 程序为载体,题目给出变量的类型和初值,然后追问:

常见问法用到的知识
某寄存器的内容是什么(十六进制)?补码 / 无符号数的机器数,模 2n2^n 运算
某变量的值是多少(十进制)?同一机器数按不同类型解释
能否用同一个加法器实现?标志位是多少?加法器与 OF、SF、ZF、CF 的生成
哪些语句的结果溢出?如何判断?溢出判断的三种方法
两个函数结果为什么不同?最大的 n 是多少?整数溢出、浮点数的有效位数与表示范围

这些问题的答案都来自一个基本事实:CPU 中只有二进制位串,“类型”只存在于编译器选择的指令和我们对结果的解释中。

  1. 写出机器数:把每个变量的初值按其类型写成 nn 位机器数(带符号整数用补码)。
  2. 在位模式上运算:加、减运算一律是 nn 位二进制加法(减法是加上减数取反加 1),结果只保留低 nn 位,即模 2n2^n。
  3. 按类型解释结果:无符号数直接按权展开;带符号数最高位权值为 −2n−1-2^{n-1};需要时再判断溢出。

同一串位,不同的解释

机器数无符号整数补码整数当作 float
8 位 86H134−128 + 6 = −122—
8 位 F6H246−10—
16 位 FFFAH65530−6—
32 位 FFFFFFFFH4 294 967 295−1NaN
32 位 80000000H2³¹−2³¹(int 最小值)−0.0
32 位 3F800000H1 065 353 2161 065 353 2161.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 开头的十六进制数时,按机器数理解。

实验C 语言类型转换:机器数如何变化
← 高位(第 31 位)低位(第 0 位)→short x1111111111111011FFFBH真值 -5(unsigned int)unsigned int11111111111111111111111111111011FFFFFFFBH真值 4294967291符号扩展得到的位
短→长:源为带符号数,做符号扩展(高 16 位填符号位 1)。扩展只看「源类型」是否带符号,与目标类型无关;扩展后按 unsigned int 解释为 4294967291

加减运算、溢出与标志位

无符号整数和带符号整数的加减法在同一个加法器上完成:加法直接相加,减法把减数各位取反、并令最低位进位 Cin=1C_{in} = 1。加法器同时输出四个标志,至于用哪个标志判断结果,由程序中变量的类型决定:

标志生成方式含义
OF(溢出)OF=Cn⊕Cn−1OF = C_n \oplus C_{n-1}(最高位进位与次高位进位异或)带符号运算是否溢出
SF(符号)结果的最高位带符号结果的正负
ZF(零)结果各位全 0 时为 1结果是否为 0
CF(进/借位)CF=Cout⊕CinCF = C_{out} \oplus C_{in}无符号运算是否溢出(加法有进位 / 减法有借位)

带符号运算溢出的三种判断方法:① 两个同号数相加,结果符号与之相反;② 双符号位(变形补码)结果为 01(正溢)或 10(负溢);③ Cn⊕Cn−1=1C_n \oplus C_{n-1} = 1。硬件中常用第 ③ 种。详见第 2 章「定点数的运算」与「加法器与运算器」。

综合题(408 真题改编)

假定在一个 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 − y86H + 0AH(F6H 取反加 1) = 90H90H无符号 144(真实差为 −112,结果错误)
z2 = x + y86H + 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) 用最高数值位的进位 Cn−1C_{n-1} 与符号位的进位 CnC_n 的异或判断:OF=Cn⊕Cn−1OF = C_n \oplus C_{n-1},为 1 表示溢出。带符号运算只有 k1、k2 两条:k1 为 −122 − (−10) 不溢出;k2 = 86H + F6H:两个负数相加得到正数 7CH(Cn=1C_n = 1,Cn−1=0C_{n-1} = 0,OF = 1),k2 溢出。

浮点数的精度与范围

IEEE 754 单精度 float 有 1 位符号、8 位阶码(偏置 127)、23 位尾数,加上隐含的最高位 1,共有 24 位有效位。由此得到两个重要结论:

  • 精度:绝对值不超过 2242^{24} 的整数都能被 float 精确表示;224+1=16 777 2172^{24} + 1 = 16\,777\,217 是第一个不能精确表示的正整数。因此 int → float 可能丢精度,而 int → double(53 位有效位)总是精确的。
  • 范围:float 的最大值为 (2−2−23)×2127≈2128(2 - 2^{-23}) \times 2^{127} \approx 2^{128},远大于 int 的最大值 231−12^{31} - 1。所以同一个计算,int 版本先溢出(结果错得离谱),float 版本先舍入(结果有微小误差),直到超过 21282^{128} 才上溢为 ∞。
阶码尾数表示
全 0全 0±0
全 0非 0非规格化数 (−1)S×0.f×2−126(-1)^S \times 0.f \times 2^{-126}
1~254任意规格化数 (−1)S×1.f×2E−127(-1)^S \times 1.f \times 2^{E-127}
全 1全 0±∞(如 7F800000H 为 +∞)
全 1非 0NaN

下面的实验对应一道经典真题:函数 f1、f2 计算 f(n)=∑i=0n2i=2n+1−1f(n) = \sum_{i=0}^{n} 2^i = 2^{n+1} - 1,区别只是变量类型。拖动 n,或点击彩色条带中的任意位置,观察两个函数在何处开始出错:

实验同一算法,int 与 float 的不同结局
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;
}
f1 (int)f2 (float)0233064100126结果精确有舍入误差整数溢出上溢为 +∞
返回值机器数状态
精确值 f(24)33,554,4312²⁵ − 1
f1(24)33,554,43101FFFFFFH结果精确
f2(24)33,554,4324C000000H有舍入误差
f1:2²⁵ − 1 ≤ 2³¹ − 1,int 能表示。f2:结果是 25 个 1,超过 24 位有效位,尾数舍入(全 1 舍入产生连锁进位,结果变成 2²⁵,比精确值大 1)。
综合题(408 真题改编)

针对上面的 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,舍入时产生连锁进位,得到 10.000…0×224=22510.000\ldots0 \times 2^{24} = 2^{25},比精确值大 1。f1(24) 是 int 运算,精确。

(4) 2³² − 1 超出了 int 的范围 [−231,231−1][-2^{31}, 2^{31} - 1],机器数为 FFFFFFFFH,按补码解释为 −1。f1(n) = 2^(n+1) − 1 ≤ 2³¹ − 1 要求 n + 1 ≤ 31,最大 n = 30。

(5) 7F800000H:阶码全 1、尾数全 0,是 +∞。float 的最大值 (2−2−23)×2127=2128−2104(2 - 2^{-23}) \times 2^{127} = 2^{128} - 2^{104};由 2n+1−1≤2128−21042^{n+1} - 1 \le 2^{128} - 2^{104} 得 最大 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) 用双符号位计算。

  1. 对阶:ΔE = 4 − 3 = 1,小阶向大阶看齐,Y 的尾数算术右移 1 位:11.0000010 → 11.10000010(多出的一位暂时保留,用于舍入),阶码变为 4。
  2. 尾数相加:11.01001100 + 11.10000010 = 10.11001110(丢弃最高位进位)。
  3. 规格化:双符号位为 10,尾数溢出,需要右规:尾数算术右移 1 位得 11.01100111|0,阶码加 1 变为 5。
  4. 舍入:保留 7 位数值位 11.0110011,被移出的最高位为 1,按 0 舍 1 入末位加 1,得 11.0110100。
  5. 判溢出:阶码 5 = 0101 在 4 位补码范围 −8~7 内,不溢出。

结果 [X + Y]浮 = 0101 1.0110100,即 −0.1001100 × 2⁵ = −19(精确值为 −19.125,误差来自舍入)。

即时练习

即时练习6 题
点击选项即可作答
  1. 1
    执行 C 语句 short si = -32767; unsigned short usi = si; 后,usi 的值是( )。
  2. 2
    某 32 位机器上 int 型变量 x 的机器数为 FFFF FFDFH,则 (unsigned) x 和 x 的值分别是( )。
  3. 3
    下列 32 位 int 型整数中,不能用 IEEE 754 单精度 float 精确表示的是( )。
  4. 4
    在 32 位机器上执行 int a = -1; unsigned b = 1;,则表达式 a < b 的值以及原因是( )。
  5. 5
    float 型变量 x 的机器数为 C0A0 0000H,则 x 的值是( )。
  6. 6
    8 位加法器计算 86H + F6H 后,标志 CF、OF、SF、ZF 依次为( )。
学完了吗?

标记完成后,侧栏会显示进度。进度仅保存在本浏览器中。