两个正数相加可能会变负?0.1+0.2不等于0.3??

本文探讨了计算机中补码表示的有符号整数为何负数表示范围比正数多一个,以及整数转换、溢出、浮点数运算中的常见问题,例如0.1+0.2不等于0.3的原因。通过实例解释了补码运算可能导致正数相加结果为负的现象,并介绍了如何在编程中处理这些问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本文并没有系统性的介绍计算机中信息的表示方式,只介绍了一些用户编程时需要注意的的小问题。

1、为什么有符号整数负数可表示范围比正数多一个?

有符号整数是用补码表示的。具体的表示形式如下,其中,向量的每个元素表示了整数的每一位。
在这里插入图片描述

负数的最高位是1,非负数最高位是0。
假设有符号整数共w位,那么最小的负数最高位为1,其他为0,即 − 2 w -2^w 2w。 最大的正数最高位为0,其他位为1,即 2 w − 1 2^w-1 2w1
因此有符号整数可表示的范围为 [ − 2 w , 2 w − 1 ] [-2^w, 2^w-1] [2w,2w1]。负数和非负数各占了取值范围的一半,而非负数中有一个数是0,这也就导致了负数和正数不对称。

2、C语言中有符号整数如何转换成无符号整数?

如果将一个有符号的整数强制类型转换成一个无符号的整数时,结果保持每个位的值不变,但改变解释这些位的方式。有符号整数是用补码方式来解释,而无符号整数则是最高位跟其他位作相同的运算。

比如,一个整数 -12345,用补码表示为 1100111111000111,而强制转换为无符号整数后,其表示依然为 1100111111000111,但解释方式变了,最后得到的结果是 53191

3、扩展整数的位

示例代码:

short sx = -12345;
unsigned short usx = sx; //53191
int x = sx;              //-12345
unsigned ux = usx;			 //53191

首先,将有符号short类型的数sx转换为无符号short,则是通过位保持不变,将补码解释改为无符号的方式,得到结果 53191

其次,将无符号short转换为更宽的类型有符号的int,则是通过执行符号扩展,在表示中添加最高有效位的值的方式,这里最高有效位为1,那么将多扩展的16位高位都置为1,得到结果 -12345

然后,将无符号short类型转换为无符号int类型,则是通过零扩展的方式,在高16位添加0,得到结果 53191

4、截断整数的位

示例代码:

int x = 53191;
short sx = (short) x; //-12345
int y = sx;           //-12345

首先,将有符号int类型转换为有符号short类型,将32位截断为16位,也就是将高16位的0全部去掉,剩余的最高有效位为1,得到结果 -12345

然后,我们又将sx强制转换为有符号int类型,采用符号扩展的方式,高16位全部置1,得到结果 -12345

然而,实际上除了C语言外,很少有语言支持无符号整数。java只支持有符号整数,并且用补码表示。java中右移运算符“>>”被定义为执行算术右移,特殊运算符“>>>”被指定为执行逻辑右移。算术右移指在左端补k个最高有效位的值,逻辑右移指在左端补k个0

5、整数运算中的溢出行为

①无符号数加法

任意两个处于 0 到 ( 2 w − 1 ) (2^w -1) (2w1)之间的无符号数 x 和 y,如果相加,可能会由于“字长膨胀”而导致溢出。溢出时,第w位(最低位为第0位,最高位为第w-1位)会被截断,那么最后显示的结果=原结果 − 2 w -2^w 2w
在这里插入图片描述

②补码加法

与无符号数加法一样,当补码加法由于字长膨胀而导致溢出时,最高位会被截断,因此也就有可能出现两个正整数相加得到负数的情况。

在这里插入图片描述

③无符号乘法

任意两个处于 0 到 ( 2 w − 1 ) (2^w -1) (2w1) 之间的无符号整数 x 和 y相乘,可能会需要2w位来表示。溢出时,高w位会被截断,因此得到的结果是原结果模 2 w 2^w 2w

在这里插入图片描述

④补码乘法

补码乘法在出现溢出时,会先将该值模 2 w 2^w 2w,再把无符号数转为补码。补码乘法和无符号乘法的位级表示相同,只解释不同。

⑤乘以常数

由于大多数机器上执行整数乘法指令非常慢,所以编译器在优化乘法操作时,通常会尝试使用移位和加法运算的组合来代替乘以成熟因子的乘法。比如:

  • x乘以2的k次方——>x左移k位。
  • x乘以14———>利用 14 = 2 3 + 2 2 + 2 1 14 = 2^3 + 2^2 + 2^1 14=23+22+21,编译器将乘法重写为 (x<<3)+(x<<2)+(x<<1)。

⑥除以2的幂

除法是比乘法更慢的操作,除以2的幂通常使用右移操作替代。无符号数采用逻辑右移,补码数采用算术右移。

整数除法总是舍入到零。并作以下规定,

  • 当x>=0, y>0,x/y得到的结果是该值向下取整。
  • 当x<0, y>0,x/y得到的结果是该值向上取整。

1)除以2的幂的无符号除法,由于采用的逻辑右移,所以比较简单。得到的结果满足上述规则。

2)但是除以2的幂的补码除法中,如果x>=0,则其算术右移的效果与逻辑右移一致。但是如果x<0,做除法时,会导致得到的结果向下取整。比如:-12340的二进制表示为1100111111001100,算术右移4位后,得到的二进制表示为1111110011111100,转换成十进制为-772,而实际上-12340/16=-771.25。

所以,我们需要做一些调整,来修正这个不合适的行为。在移位之前“偏置”这个值,即,x右移k位,执行表达式**(x+(1<<k)-1)>>k**,就可以得到期待的值。原理是:对于整数x和y (y>0),(x/y)向上取整=[(x+y-1)/y]向下取整。

6、0.1+0.2不等于0.3

如果想要把一个十进制的小数,比如0.1,转换成二进制,会出现无限循环的情况。

将一个十进制小数转换成二进制,通常的做法是,将十进制小数乘2,得到的积取整数部分,再用余下的小数部分再乘2,再将积的整数部分取出……直到积中的小数部分为0。比如,十进制的0.625

0.625 * 2 = 1.25 整数部分为 1

0.25 * 2 = 0.5 整数部分为 0

0.5 * 2 = 1 整数部分为1

对应的二进制是0.101

但是十进制的0.625是一种特殊的情况,其可以转换成有限位的二进制数。然而对于大部分小数来说,转换时会出现无限循环的情况。比如,十进制的0.1对应的二进制数为 0.000110011001100……。

因此,计算机无法精确地用二进制方式表示0.1。

IEEE 754 提出使用有限位的浮点数近似表示小数。C语言中单精度的浮点数 float 共32位,符号位s占1位,阶码e占8位,小数字段m占23位。双精度的浮点数 double 共64位,符号位占1位,阶码占11位,小数字段占52位。表示方式为 n = ( − 1 ) s ∗ m ∗ 2 e n=(-1)^s * m * 2^e n=(1)sm2e

十进制小数0.1,对应的双精度浮点数的二进制表示为0.00011001100110011001100110011001100110011001100110011001,

十进制小数0.2,对应的双精度浮点数的二进制表示为0.00110011001100110011001100110011001100110011001100110011,

0.1+0.2所得结果的双精度浮点数的二进制表示为0.01001100110011001100110011001100110011001100110011001100,对应的十进制数为0.30000000000000004

如果有高精度运算需求,不要使用float或double进行运算,尤其是涉及资金方面的。那么,如何解决这样的需求呢?

java中提供了BigDecimal来进行精确运算。

7、浮点数强制转换

已知,单精度的浮点数float共32位,符号位占1位,阶码占8位,小数字段占23位。双精度的浮点数double共64位,符号位占1位,阶码占11位,小数字段占52位。

  • 从int转换为float,数字不会溢出,但是会被舍入,因为float的小数字段只有23位的精度。
  • 从int或float转换为double,由于double的小数字段占52位,精度很高,所以能够保留精确的数值。
  • 从double转换为float,不仅精度可能缩小,会被舍入,而且还会出现溢出成+∞或-∞。
  • 从float或者double转换为int,值会向0舍入。

参考资料:《深入理解计算机系统》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值