编程基础之机器数和算术溢出

一文深入理解计算机底层数据存储的形式,以及数据存储中的机器数(原码,反码,补码)和算术溢出等概念。

更多相关编程基础内容,请关注博主 Hexo 博文系列:

计算机中信息的存储

编码详解

进制详解

机器数和算术溢出


预备知识

我们知道,由于计算机的硬件决定,任何存储于计算机中的数据,计算机底层存储数据时都使用的是二进制形式。详情请参考 >>>> 【计算机中信息的存储】。

并且,

[1] >>>> 计算机只有加法运算器

根据冯·诺依曼提出的经典计算机体系结构框架,一台计算机由运算器、控制器、存储器、输入和输出设备组成。

目前,计算机中运算器只有加法运算器,没有减法运算器(据说一开始是有的,后来由于减法运算器硬件开销太大,被废了)。那如何进行减法运算???

我们知道,减去一个数可以看作加上这个数的相反数,计算机中没办法直接做减法的,但是可以通过加法运算来实现减法功能。

有个前提 >>>> 必须要有负数的概念,这就得向二进制数中引入符号位了。

| ================================================== Split Line =============================================== |

[2] >>>> 机器数 && 真值

机器数 >>> 一个数在计算机的存储形式是二进制数,我们称这些二进制数为机器数。机器数是有符号,在计算机中用机器数的最高位存放符号位,0 表示正数,1 表示负数。

由于存在符号位,以机器数 1000 0111 为例,机器数的形式值(135,无符号数)不等于其真正的值(-7,有符号数)。

机器数的真值 >>> 将带符号的机器数的真正表示的值称为机器数的真值(有符号数值)。

| ================================================== Split Line =============================================== |

[3] >>>> 内存存储的是二进制补码

计算机在存储一个数字时,并不是直接存储该数字对应的二进制数字(原码),而是存储该数字对应二进制数字的补码。

原码、反码、补码的产生过程就是为了解决计算机做减法和引入符号位的问题(后续会告诉你为什么)。


原码 && 反码 && 补码

这一小节来看原码、反码和补码到底是个啥?

为了方便说明问题,这里我们假定用 4 位二进制数表示一个整数。

原码

原码的表示与机器数真值表示的一样,即用第一位表示符号,其余位表示数值。

例如,十进制的的正负 1,用 4 位二进制的原码表示如下:

【+1】= 原:[ 0001 ]

【-1】= 原:[ 1001 ]

所有机器数的原码表示如下:

可以看到,使用原码来表示一个有符号数会带来两个问题:

  • 第一个问题 >>> 正负相加不等于零:1 + (−1) –> 0001 + 1001=1010,按照原码表示等于 −2;
  • 第二个问题 >>> 同时存在两个零,分别为 00001000

可见,原码不适合用来表示有符号数!!!


为了保证正负相加等于零,引入了反码:

反码

反码的表示方法为:

  • 正数的反码是其原码本身;
  • 负数的反码是在其原码的基础上,符号位不变,其余按位取反。

【+1】= 原: [ 0001 ] = 反:[ 0001 ]

【-1】 = 原:[ 1001 ] = 反:[ 1110 ]

对照上面原码表(看成一个钟表),你可以将机器数 1000 想象成钟表中的 12 点,把机器数 0000 想象成钟表中 6 点,我们知道正数的反码是其本身,这也就意味着左边表盘不变动。那么,如何变动右表盘可以使得正负相加等于零???

采用反码的表示方法,负数 >>> 原码就是从 12 点开始 【顺时针】 排列 -1-7,其反码是从 6 点开始 【逆时针】 排列 -1-7。反码表如下:

可以看到,第一个问题解决了(正负相加等于零),但是第二个问题还是没有解决,依然存在两个零,分别为 00001111

补码就是为了解决这个问题而发明的….


聪明如你肯定想到,好解决啊~让负数反码整体顺时针移动一位,把 -0 “顶掉” 就可以了,这样对于位置 1000 还空出来一个,还可以用来多存放一个数值。

补码

事实上, 你可以如此理解~~~

补码的表示方法为:

  • 正数的补码是其原码本身;
  • 负数的补码是在其原码的基础上,符号位不变,其余按位取反后加 1(即在反码的基础上加1)。

【+1】= 原: [ 0001 ] = 反:[ 0001 ] = 补:[ 0001 ]

【-1】 = 原:[ 1001 ] = 反:[ 1110 ] = 补:[ 1111 ]

对于位置 1000 空出的位置还可以用来表示 -8,直接给出补码表:

注意 >>> 实际上,还使用以前的 -0 的补码来表示 8,所以 8 并没有原码和反码表示,只要补码是 [1000],其十进制数值就为 8

在验证以下第一个问题:如果丢弃最高位的进位,结果满足正负相加等于零。Niu 啊~~~


牢记 4 位机器数的补码表规律,并将其推到更一般的 8 位机器数 >>>

8 位机器数原码、反码可表示范围:[-127, -1],[10000000(-0)],[0, 127],共 2^8 = 256 种组合。

8 位机器数补码可表示范围:[-128, -1][0, 127],注意 [100000000(-128)] 没有原码和反码表示。这也可以解释为什么计算机中一个字节的取值范围是 [-128,127]。

已知补码如何求原码 >>>>

当补码最高位为 0 时,即该数是正数,正数的原码、反码、补码都是一样的。

当补码最高位为 1 时,该数为负数,再对补码求补码即为原码。


算术溢出

这里我们来讨论一个在实际编写程序中非常危险的一个bug,例如 C 语言种的整型数溢出问题。

我们知道,有符号数在计算机内部都是通过补码来表示的。

上述补码表中可以看出,如果数值加上 x,就代表着顺时针走 x 格;如果数值减去 x,就代表着逆时针走 x 格。

如果 3 + 1,那就是从 3 顺时针走一格,等于 4,这是没有任何问题。但是如果是 7 + 1 呢?顺时针走 1 格后,变成了 −8 了。如果 7 + 7 呢,顺时针走 7 格,等于 −2 了。这就是整型数发生了溢出。

所谓的溢出,就是因为 4 位二进制数,用补码表示一个整数的时候,所能表示的最大正整数就是 2^(4-1)-1=7,如果在 7 的基础上再加 1,就会发生 “轻微溢出”,变为了最小的那个负数;如果两个最大的正整数相加,就会发生“严重溢出”。同理,最小的负数 -8 逆时针走 1 位,以及走 8 位对应如上。

所谓的 “轻微溢出” 和 “严重溢出” 都是从溢出的角度去定义的。事实上,它们都是非常严重的 bug,在实际编程中并不是说 “严重溢出” 就比 “轻微溢出” 更严重。

=======================

参考链接:https://www.jianshu.com/p/ffc97c4d2306


Author

Waldeinsamkeit

Posted on

2016-07-29

Updated on

2024-01-13

Licensed under

You need to set install_url to use ShareThis. Please set it in _config.yml.

Comments

You forgot to set the shortname for Disqus. Please set it in _config.yml.