5780 字
29 分钟
2.3浮点数的表示和运算

浮点数表示法通过将比例因子嵌入数据中,使小数点位置可根据需要浮动。这样,在有限位数下,既能扩大数值的表示范围,又能保持较高的有效精度。例如,用定点数表示电子质量(9×10289\times10^{-28}g) 或太阳质量 (2×10332\times10^{33}g) 极为不便,而浮点数则能高效处理此类极大或极小的数值。

通常,浮点数表示为

N=(1)S×M×REN = (-1)^S \times M \times R^E

其中,SS (取值0或1)决定浮点数的符号MM 是一个非负的定点小数,称为尾数,通常用原码表示;EE 是一个定点整数,称为阶码(或指数),通常采用偏置表示(一种移码形式)。RR 是基数(通常隐含约定为2、4或16)。可见,浮点数由符号尾数阶码三部分组成。

在IEEE754浮点数标准广泛使用之前,不同计算机所用的浮点数表示格式各不相同。

其中,第0位为符号 SS;第17位为阶码 EE,采用偏置值为64的移码表示;第831位为24位尾数 MM,以二进制原码小数表示;基数 RR 为2。在该格式中,阶码的值决定了小数点的实际位置;阶码的位数决定了浮点数的表示范围;尾数的位数则决定了数值的精度。

2.3.1 IEEE 754标准的浮点数#

1. IEEE 754 标准的浮点数格式#

现代系统普遍采用IEEE754浮点数标准。该标准定义了两种常用格式:32位单精度浮点数(float型)和64位双精度浮点数(double型),其基数隐含为2,如下图。

精度类型符号位 (bit)阶码位 (bit)尾数位 (bit)总位数
32 位单精度182332
64 位双精度1115264

图2.14 IEEE754标准浮点数的格式

32位单精度格式包含1位符号 ss、8位阶码 ee 和23位尾数 ff;64位双精度格式包含1位符号 ss、11位阶码 ee 和52位尾数 ff。基数隐含为2;尾数用原码表示。对于规格化的二进制浮点数,尾数的最高位恒为1。为提升精度,IEEE754IEEE754 不显式存储该位,而是将其隐含在小数点之前,称为隐藏位。因此,单精度格式的23位尾数实际提供了24位有效数字,双精度格式的52位尾数实际提供了53位有效数字。例如,(12)10=(1100)2(12)_{10} = (1100)_2,规格化后为 1.1×231.1 \times 2^3。其中,小数点前的“1”不实际存储,尾数 ff 仅保存小数部分 1000100\cdots0 ,而阶码保存的是指数3的编码值。

IEEE754IEEE754 标准的阶码采用移码表示,但偏置值并不是通常 nn 位移码所用的 2n12^{n-1},而是 2n112^{n-1}-1。因此,单精度和双精度格式的偏置值分别为127和1023。上例中,指数真值为3,因此在单精度格式中,阶码为 127+3=130127+3=130 (82H);在双精度格式中,阶码为 1023+3=10261023+3=1026 (402H)。

TIP

阶码(指数)的存储方式

在 IEEE 754 标准中,阶码以**偏移量(移码)**形式存储,而非补码或原码:

  • 32位单精度:阶码真值 E 需加上 127,然后转换为无符号二进制(8位)。
  • 64位双精度:阶码真值 E 需加上 1023,然后转换为无符号二进制(11位)。

公式:
存储的阶码 = E + 偏移量 (其中偏移量 = 127 或 1023)

IEEE 754标准的规格化单精度浮点数的真值为

(1)s×1.f×2e127(-1)^s \times 1.f \times 2^{e-127}

规格化双精度浮点数的真值为

(1)s×1.f×2e1023(-1)^s \times 1.f \times 2^{e-1023} \quad

其中,规格化单精度浮点数的阶码取值范围为 12541\sim254 (8位,全0和全1保留用于特殊值);规格化双精度浮点数的阶码 ee 的取值范围为 120461\sim2046 (11位,保留用途相同)。

2. IEEE754格式浮点数的表示范围#

IEEE754规格化浮点数的表示范围见表2.2。

表2.2 IEEE754规格化浮点数的表示范围

格式最小值最大值
单精度e=1,f=0e=1, f=0
1.0×21127=21261.0 \times 2^{1-127} = 2^{-126}
e=254,f=.111e=254, f=.111\ldots
1.1111×2254127=2127×(2223)1.111\ldots 1 \times 2^{254-127} = 2^{127} \times (2 - 2^{-23})
双精度e=1,f=0e=1, f=0
1.0×211023=210221.0 \times 2^{1-1023} = 2^{-1022}
e=2046,f=.1111e=2046, f=.1111\ldots
1.1111×220461023=21023×(2252)1.111\ldots 1 \times 2^{2046-1023} = 2^{1023} \times (2 - 2^{-52})

当浮点运算结果的绝对值超过最大规格化数时,发生上溢(也称溢出),可分为:

  • 正上溢:若结果为正且大于最大规格化正数。
  • 负上溢:若结果为负且小于最小规格化负数(绝对值过大)。

IEEE 754对上溢的处理规则:

  1. 将结果设为 ++\infty-\infty
  2. 置位浮点溢出异常标志,IEEE 754规定,默认情况下不触发异常中断,程序继续执行,除非显式开启此类异常响应。

当运算结果的绝对值小于最小规格化正数但不为零时,发生下溢,可分为:

  • 正下溢:若结果为正,且处在0到最小规格化正数之间。
  • 负下溢:若结果为负,且处在最大规格化负数到0之间。

对下溢的处理采用渐进下溢机制:

  1. 若结果落在非规格化数可表示范围内,则以非规格化形式存储,保留部分有效精度;
  2. 若结果过于接近零(舍入后为零),则存储为+0或-0,并置位浮点下溢异常标志。同样,默认不响应下溢异常,程序继续运行,除非显式启用异常处理。

3. 几种特殊的IEEE754浮点数#

在IEEE754标准中,当阶码全为0或全为1时,浮点数具有特殊含义,如表2.3所示。

表2.3 阶码全为0或全为1时IEEE754浮点数的解释

值的类型单精度(32位)双精度(64位)
符号阶码尾数符号阶码尾数
正零00000000
负零100-0100-0
正无穷大0255(全1)0++\infty02047(全1)0++\infty
负无穷大1255(全1)0-\infty12047(全1)0-\infty
无定义数(非数)0或1255(全1)f0f \neq 0NaN0或12047(全1)f0f \neq 0NaN
非规格化正数00f0f \neq 02126×(0.f)2^{-126} \times (0.f)00f0f \neq 021022×(0.f)2^{-1022} \times (0.f)
非规格化负数10f0f \neq 02126×(0.f)-2^{-126} \times (0.f)10f0f \neq 021022×(0.f)-2^{-1022} \times (0.f)
  1. 全0阶码全0尾数+0/-0。符号 ss 决定其正负,通常情况下+0和-0是等效的。
  2. 全1阶码全0尾数++\infty / -\infty++\infty 在数值上大于所有有限数,-\infty 则小于所有有限数。引入无穷大数的目的是,使程序在溢出等异常情况下仍能继续执行。
  3. 全1阶码非0尾数NaN (Not a Number)。表示一个没有定义的数,称为非数
  4. 全0阶码非0尾数非规格化数。非规格化数的特点是阶码为全0,不使用隐藏位,尾数字段不全为0。因此,单精度和双精度浮点数的指数分别为-126-1022。非规格化数用于实现渐进下溢,填补0与最小规格化数之间的数值间隙。

【例2.5】 将十进制数-8.25转换为IEEE754单精度浮点数格式表示。

解:

IEEE 754单精度浮点数的偏置值是127;尾数最高位的“1”是被隐藏的。

先将-8.25转换为二进制,即 1000.01=1.00001×23-1000.01 = -1.00001 \times 2^3,尾数部分取小数点后的23位(00001后补0至23位);再计算阶码 EEE127=3E-127=3,因此 E=130E=130,转换为二进制为1000 0010。

IEEE754单精度浮点数格式:符号(1位)+阶码(8位)+尾数(23位),即为

1;1000 0010;0000 1000 0000 0000 0000 000

因此,其单精度浮点数格式表示为1100 0001 0000 0100 0000 0000 0000 0000 = C104 0000H。

【例2.6】 求IEEE754单精度浮点数C640 0000H的值是多少。

解:

先将C640 0000H按二进制展开为1100 0110 0100 0000 0000 0000 0000 0000

按IEEE754单精度浮点数格式划分:

符号阶码尾数
11000 11001000 0000 0000 0000 0000 000

因此,符号=1表示负数;阶码真值为 1000110001111111=(00001101)2=1310001100 - 01111111 = (00001101)_2 = 13 ;尾数真值为 1+(0.1)2=1.51 + (0.1)_2 = 1.5(注意,尾数含隐藏位1)。因此,该单精度浮点数的值为 1.5×213-1.5 \times 2^{13}

2.3.2 浮点数的加减运算#

浮点数运算的特点是阶码与尾数分开处理,浮点数加减运算分为以下几个步骤。

1. 对阶#

对阶的目的是使两个操作数的小数点位置对齐,即令它们的阶码相等,以便尾数可以直接相加减。对阶的原则是:小阶向大阶看齐,即将阶码较小的数的尾数右移,右移位数等于两阶码差的绝对值。对于IEEE754标准的浮点数,对阶时需要进行移码减法运算,以求得阶码差。尾数右移时,仅移动数值位,符号位不参与移位;对于规格化数,隐藏位1会随尾数右移而进入小数部分,空出的高位补0。为保证运算精度,移出的低位不应丢弃,而应保留并参与后续尾数运算。

NOTE

若采用大阶向小阶看齐,则需将尾数左移,导致最高有效位被移出,造成不可逆的精度错误。

2. 尾数加减#

由于IEEE754标准采用定点原码小数表示尾数,因此尾数加减运算实质上是定点原码小数的加减运算,应根据相应的规则执行。对于规格化数,在运算前还需要将隐藏位还原到尾数部分,形成完整的 1.f1.f 形式。此外,对阶过程中为保持精度而保留的附加位也要参与尾数加减运算。

3. 尾数规格化#

为在浮点运算中最大限度保留有效数字,需要对运算结果进行规格化处理。所谓规格化,是指通过调整尾数与阶码,使浮点数的尾数满足最高有效位为1的形式。

IEEE754规格化尾数的形式为 1.××1.\times\ldots\times。尾数相加减后可能出现两类非规格化结果:

1.××+1.××=1×.××1.\times\ldots\times + 1.\times\ldots\times = 1\times.\times\ldots\times1.××1.××=0.001××1.\times\ldots\times - 1.\times\ldots\times = 0.0\ldots01\times\ldots\times
  1. 右规:当结果为 1×.××1\times.\times\ldots\times 时,需要进行右规。尾数每右移一位,阶码加1。尾数右移时,最高位1被移到小数点前一位作为隐藏位,最后一位移出时,要考虑舍入。

  2. 左规:当结果为 0.001××0.0\ldots01\times\ldots\times 时,需要进行左规。尾数每左移一位,阶码减1。可能需要左规多次,直到将第一位1移至小数点左边。

NOTE
  • 左规一次相当于乘以2,右规一次相当于除以2;
  • 需要右规时,只需进行一次。

4. 舍入#

在对阶和右规过程中,尾数右移可能导致低位丢失。为保证精度,移出的低位通常被保留用于中间计算。最终结果需通过舍入处理,还原为标准的IEEE754格式。

为此,IEEE754引入三个辅助位以指导精确舍入。

  1. 保护位:紧邻尾数最低有效位之后的第一位,用于初步判断舍入方向。
  2. 舍入位:位于保护位之后,与保护位和粘滞位共同构成完整的舍入信息。
  3. 粘滞位:只要舍入位之后被移出的位中存在至少一个1,粘滞位就置为1,否则为0。

IEEE 754定义了四种可选的舍入模式。

  1. 就近舍入(默认方式):选择最接近真实值的可表示数。若真实值恰好位于两个可表示数的正中间,则选择尾数最低有效位为0的那个(偶数)。具体规则:

    1. 若保护位=0,直接舍去;

    2. 若保护位=1且(舍入位=1或粘滞位=1),则尾数加1;

    3. 若保护位=1、舍入位=0、粘滞位=0,则在尾数末位为奇数时向其加1,以符合向偶数舍入的要求。例如,运算后得到浮点数的临时尾数 M1M_1,舍入过程如下:

      M1=1.10110011100110011010101 0 1 0 1M_1 = 1.10110011100110011010101\ 0\ 1\ 0\ 1

      注意,下划线部分为保留的23位尾数,其后依次为保护位、舍入位、粘滞位。

      由于保护位=1、舍入位=0、粘滞位=1,结果属于非中间值,需要向尾数加1。加1后的23位尾数为10110011110011001101011。

      若运算后得到临时尾数 M2M_2,则舍入过程如下:

      M2=1.10110011100110011010101 0 0 0M_2 = 1.10110011100110011010101\ 0\ 0\ 0

      由于保护位=1、舍入位=0、粘滞位=0,结果恰好位于两个可表示数的正中间。此时尾数最低有效位为偶数,无须加1。最终的23位尾数保持为1011 0011 1100 1100 1101 010。

  2. 正向舍入:朝数轴 ++\infty 方向舍入,即选择数值更大的可表示数。

  3. 负向舍入:朝数轴 -\infty 方向舍入,即选择数值更小的可表示数。

  4. 截断法:直接截取所需位数,丢弃后面的所有位,实现最为简单。对正数或负数来说,都是选择更接近原点的那个可表示数,也称为朝原点舍入

5. 溢出判断#

在尾数规格化或舍入过程中,可能对阶码进行加减操作,因此需要判断指数是否溢出。在IEEE754中,浮点数的溢出由阶码是否超出可表示范围决定;尾数溢出可通过右规修正,而真正的溢出仅发生在阶码上溢或下溢时。

  1. 上溢判断。尾数相加后若结果 2\ge 2,或舍入时尾数末位加1引发进位(如 1.111+1=10.0001.111\cdots+1=10.000\cdots),则均需右规:尾数右移一位,阶码加1。若原阶码已为最大正规格化值(单精度阶码字段为11111110,对应真值+127),加1后变为11111111(该编码保留用于表示无穷大或NaN),则视为指数上溢,通常会引发异常。

  2. 下溢判断。左规时尾数左移,阶码减1。若阶码真值减至低于最小正规格化值(单精度-126,双精度-1022),则进入非规格化数范围(阶码字段为0)。若结果进一步小于最小可表示非规格化数(如 21492^{-149}210742^{-1074}),则视为指数下溢,通常将结果置为机器零。

【例2.7】xxyy 为float型变量,x=10.5x=10.5y=120.625y=-120.625,请给出 x+yx+y 的计算过程。

解:

x=10.5=(1010.1)2=(1.0101)2×23x=10.5=(1010.1)_2=(1.0101)_2 \times 2^3。其IEEE754单精度:符号位为0;阶码为 3+127=1303+127=130,即10000010;机器数(注意隐含尾数最高位)为 0;1000 0010;010 0000 0000 0000 0000 00000;1000 \space 0010;010 \space 0000 \space 0000 \space 0000 \space 0000 \space 0000

y=120.625=(1111000.101)2=(1.111000101)2×26y=-120.625=-(1111000.101)_2=-(1.111000101)_2 \times 2^6。其IEEE754单精度:符号位为1,阶码为 6+127=1336+127=133,即10000101;机器数为 1;1000 0101;111 0001 0100 0000 0000 00001;1000 \space 0101;111 \space 0001 \space 0100 \space 0000 \space 0000 \space 0000

  1. 对阶。求阶差 ExEy=3E_x - E_y = -3。故将 xx 的尾数右移3位,阶码调整为 Ey=133E_y=133。对阶后,xx 的尾数变为 0.0010 1010 0000 0000 0000 000 0 0 00.0010 \space 1010 \space 0000 \space 0000 \space 0000 \space 000 \space 0 \space 0 \space 0(含保留的附加位),此时无隐藏位。

  2. 尾数相加。0.0010 1010 0000 0000 0000 000 0 0 0+(1.1110 0010 1000 0000 0000 000)=1.1011 1000 1000 0000 0000 000 0 0 00.0010 \space 1010 \space 0000 \space 0000 \space 0000 \space 000 \space 0 \space 0 \space 0 + (-1.1110 \space 0010 \space 1000 \space 0000 \space 0000 \space 000) = -1.1011 \space 1000 \space 1000 \space 0000 \space 0000 \space 000 \space 0 \space 0 \space 0(注意,附加位参与运算,但不会存储)。

  3. 规格化。尾数相加结果 1.1011100010×26-1.1011100010\cdots \times 2^6,已是规格化形式。

  4. 舍入。单精度尾数保留23位,附加位全为0,按就近舍入规则,直接截断。因此,x+yx+y 的机器数为 1;1000 0101;1011 1000 1000 0000 0000 0001;1000 \space 0101;1011 \space 1000 \space 1000 \space 0000 \space 0000 \space 000。其真值为 (1.101110001)2×26=(1101110.001)2=110.125-(1.101110001)_2 \times 2^6 = -(1101110.001)_2 = -110.125

2.3.3 C语言中的浮点数类型#

C语言中的 float 型和 double 型分别对应 IEEE 754 单精度和双精度浮点数。long double 型通常对应扩展双精度格式,其长度和格式依赖于编译器与目标平台。在C语言中,表达式中的赋值、运算或比较操作会触发自动类型转换,常见的转换序列为 charintlongdouble\operatorname{char} \to \operatorname{int} \to \operatorname{long} \to \operatorname{double}floatdouble\operatorname{float} \to \operatorname{double} [OCR存疑],这些转换通常由范围和精度较低的类型向更高者进行,一般不会丢失信息。

当不同类型的数据混合运算时,遵循类型提升原则:较低类型自动转换为较高类型。例如,longint 运算时,先将 int 转换为 long,然后进行运算,结果为 longfloatdouble 运算时,先将 float 转换为 double,结果为 double。这类由编译器自动完成的转换称为隐式类型转换

  1. intfloat 时,虽然不会发生溢出,但由于 float 尾数(含隐藏位)仅24位有效,而 int 为32位,当整数值的二进制有效位超过24位时,需做舍入处理,导致精度损失。

  2. intfloatdouble 时,因 double 的有效位更多,通常能精确表示原值。

  3. doublefloat 时,一方面 float 的表示范围较小,大数值转换时可能发生溢出;另一方面 float 的尾数有效位变少,高精度数转换时会发生舍入误差。

  4. floatdoubleint 时,由于 int 没有小数部分,小数部分被直接丢弃(向零截断);同时,若浮点数值超出 int 的表示范围,则会发生整数溢出。

不同数据类型之间的转换常隐藏不易察觉的精度损失或溢出风险,编程时需格外谨慎。

2.3.4 数的宽度和存储 [注:此标题与2.3.4小节标题文字有出入,依据原文保留]#

1. 数据的宽度和单位#

在计算机中,比特(bit,也称位,符号为b)是最小的信息单位,表示一个二进制位(0或1);字节(byte,符号为B)是基本的存储和寻址单位,1字节=8比特。随着信息规模增大,常在B(字节)或b(位)前添加前缀如KB、MB、GB等。在传统计算机系统中,这些前缀通常按2的幂定义,如 1KB=210B=1024B1KB=2^{10}B=1024B

此外,(word)也是常用的数据组织单位。它是由体系结构定义的逻辑单位,通常用于表示整数、地址等基本数据类型的宽度,其长度因架构而异,常见的有2、4或8字节。

与字不同,字长(也称机器字长)指CPU内部整数运算的数据通路的宽度,通常等于通用寄存器的宽度。字长反映计算机一次能处理的整数数据的位数,是衡量机器性能的重要指标。日常所说的“32位机”或“64位机”,其中的32或64即指字长。例如,在Intel x86架构中,自80386起字长为32位(32位机),但其体系结构仍将16位定义为一个字,32位称为双字。这表明:字是架构层面的约定,而字长体现的是硬件的实际处理能力。

2. 数据的“大端方式”和“小端方式”存储#

在存储数据时,数据从低位到高位可以按从左到右排列,也可以按从右到左排列。因此,不宜用最左或最右来表征数据的最高位或最低位,通常使用最低有效字节(LSB)最高有效字节(MSB)来分别表示数据的最低位和最高位。例如,在32位计算机中,一个 int 型变量 i 的机器数为01234567H,其最高有效字节MSB=01H,最低有效字节LSB=67H。

现代计算机普遍采用字节编址,即每个地址对应1字节。不同类型的数据占用不同字节数(如 intfloat 占4字节,double 占8字节),而程序中每个变量仅分配一个起始地址。假设变量 i 的地址为0800H,那么其4个字节01H、23H、45H、67H将占据连续的4个内存单元。这些字节在内存中的排列方式分为两种:

大端方式
地址0800H0801H0802H0803H
数据01H23H45H67H
小端方式
地址0800H0801H0802H0803H
数据67H45H23H01H
  1. 大端方式(big endian):MSB存储在低地址,LSB存储在高地址,字节顺序与数值的标准十六进制书写顺序一致。

  2. 小端方式(little endian):LSB存储在低地址,MSB存储在高地址,字节顺序与标准书写顺序相反。

在分析机器代码时,需特别注意字节顺序。例如,以下是由反汇编器生成的一行代码:

4004d3: 01 05 64 94 04 08 add eax, 0x08049464

其中,4004d3是指令地址,01 05 64 94 04 08 是指令的机器码,add eax, 0x08049464 是其汇编形式。指令的第二个操作数是立即数 0x08049464,其在内存中按地址递增顺序存储为:64H、94H、04H、08H。由于低地址存放的是LSB(64H),高地址存放的是MSB(08H),符合小端方式的特征。将这4个字节按小端规则重组,即可得到正确的立即数 0x08049464。因此,在阅读小端机器代码时,需要将连续字节按逆序组合才能还原其逻辑数值。

3. 数据按“边界对齐”方式存储#

在字长为32位的系统中,边界对齐要求数据的存储地址是其对齐值(通常等于该类型大小,单位:字节)的整数倍:字节可位于任意地址,半字地址须为2的倍数,字地址须为4的倍数。满足此条件时,CPU可通过一次访存读取完整数据;否则,若数据跨越两个存储单元,则需两次访存并拼接字节,显著降低效率。为满足对齐要求,编译器会在必要时填充空白字节。这种“以空间换时间”的策略虽略微增加内存占用,但能大幅提升访问速度。

C语言中,struct 型的内存布局遵循以下对齐规则:

  1. 每个成员的起始地址必须是其对齐值的整数倍(例如:char 为1,short 为2,int 为4);
  2. 整个结构体的大小必须是其最大成员对齐值的整数倍(不足则在尾部填充)。这确保了每个结构体成员的起始地址均满足对齐要求。

先看两个例子(基于32位x86环境,GCC编译器):

struct A { struct B {
int a; char b;
char b; int a;
short c; short c;
} }

结果却是:sizeof(A)=8sizeof(B)=12

设B从地址0x0000开始,成员b的对齐值是1,其存放地址符合 0x0000%1=00x0000\%1=0;成员a的对齐值是4,需对齐到4字节边界,故起始于0x0004,占据 0x00040x00070x0004\sim 0x0007;成员c的对齐值是2,起始于 0x00080x0008,占据 0x00080x00090x0008\sim 0x0009。此外,结构体长度必须是最大对齐值(4)的整数倍,当前大小10字节,需填充至12字节 (0x000A0x000B0x000A\sim 0x000B)。

设A也从地址0x0000开始,成员a的对齐值是4,存放在 0x00000x00030x0000\sim 0x0003;成员b的对齐值是1,存放在0x0004;成员c的对齐值是2,为满足“起始地址 %\% 对齐值 =0=0”的条件,只能存放在 0x00060x00070x0006\sim 0x0007,总大小为8字节,无须尾部填充。

精简指令集计算机(RISC)普遍采用边界对齐,以支持高效的指令流水线。

评论