谈谈计算机中的整数

  1. 前言
  2. 整形长度
  3. 整数在内存中的存储形式
  4. 整形存储的原理探究

一:前言

整数是从小学就开始学习的内容,作为程序员,整形是平时玩的最不亦乐乎的东西。这篇博文,内容基本都在大学计算机基础书本中出现,这里就算做个人笔记,加深记忆~

二:整形长度

以C语言为例,整形是int。一般占用4个字节,即可以表示2^32个数字,大约43亿。虽然已经很大了,不过如果要表示更大的数字,比如银河系的星球个数,那就需要存储量更大的数据类型,于是便出现了long。如果要存储的数据是比较小的,为了不浪费内存空间,又出现了short类型。三者的关系是:

short 至少占用 2 个字节。
int 建议为一个机器字长。32 位环境下机器字长为 4 字节,64 位环境下机器字长为 8 字节。
short 的长度不能大于 int,long 的长度不能小于 int。

也就是说,short可能和int一样大,int也可能和long一样大。

三:整数在内存中的存储形式

C语言规定,int在内存中的最高位为符号位,0~30 位表示数值,31 位表示正负号。当然,如果明确数据的范围是正数,C语言也提供了无符号数,即只能表达正数范围的整数。无符号数没有符号位,相当于32位都是表示数值,所以无符号数表示正数的范围是有符号数的两倍。

整形在内存中的存储

加减法是最基本的运算,所以在计算机中直接由硬件提供,所以硬件的设计要尽量简单。有符号数因为有符号位,计算机要专门识别符号位和数值位,无疑加大了硬件电路的复杂度,所以,人们想出了两个优化目标:

1.让符号位也参与运算,简化电路
2.加法和减法统一

那如何优化呢?这要先从几个概念说起~

  1. 原码:
    将一个整数转换成二进制形式,就是其原码。例如short a = 7;,a 的原码就是0000 0000 0000 0111。
    通俗的理解,原码就是一个整数本来的二进制形式。

  2. 反码:
    对于正数,它的反码就是其原码(原码和反码相同);负数的反码是将原码中除符号位以外的所有位(数值 位)取反,也就是 0 变成 1,1 变成 0。例如short a = 6;,a 的原码和反码都是0000 0000 0000 0110;更改 a 的值a = -18;,此时 a 的反码是1111 1111 1110 1101。

  3. 补码:
    对于正数,它的补码就是其原码(原码、反码、补码都相同)。负数的补码是其反码加 1。例如short a = 6;,a 的原码、反码、补码都是0000 0000 0000 0110;更改 a 的值a = -18;,此时 a 的补码是1111 1111 1110 1110。

原码、反码、补码的概念只对负数有实际意义,对于正数,它们都一样。

所以,将整数写入计算机内存,会将原码转化为补码,读取整数的时候,会将补码转化为原码。
(以下为了方便,整数的表示使用1-2个字节表示)

如果要让符号位也参与运算,并且加法和减法统一,正数直接使用原码是没有问题的,但是如何有负数,就不正确了,例如:

1 - 1 = 1 + (-1) = [00000001]原 + [10000001]原 = [10000010]原 = -2

显然是错误的。

为了解决原码做减法的问题, 出现了反码:

计算十进制的表达式: 1-1=0

1 - 1 = 1 + (-1) = [0000 0001]原 + [1000 0001]原= [0000 0001]反 + [1111 1110]反 = [1111 1111]反 = [1000 0000]原 = -0

是不是反码就可以解决问题了呢?看下面例子:

13 - 5 = 13 + (-5)
= [0000 0000 0000 1101]原 + [1000 0000 0000 0101]原
= [0000 0000 0000 1101]反 + [1111 1111 1111 1010]反
= [1 0000 0000 0000 0111]反
= [0000 0000 0000 0111]反
= [0000 0000 0000 0111]原
= 7

显然是错的。但是5-13使用反码的计算结果是正确的,这个读者可以自行验证~~

上面的例子,其实是为了说明使用反码计算的两个明显的问题

  1. 绝对值大的数减去绝对值小的数结果会比正确结果少1
  2. 可能出现 原码1000 0000,即-0的尴尬结果

为了解决以上两个问题,勤劳勇敢更聪明的计算机设计者们设计出了补码。

使用补码计算13-5:
13 - 5 = 13 + (-5)
= [0000 0000 0000 1101]补 + [1111 1111 1111 1011]补
= [1 0000 0000 0000 1000]补
= [0000 0000 0000 1000]补
= [0000 0000 0000 1000]反
= [0000 0000 0000 1000]原
= 8

结果就是正确的了。为什么呢?
补码其实就是反码加1(针对负数而言),绝对值大的数减去绝对值小的数,结果为正数,整个过程负数反码转为补码只有一次(被减数转为补码),所以就是相当于反码计算的结果加1,。而绝对值小的数坚绝对值大的数,结果为负数,整个过程负数反码转为补码有1次,补码转为原码1次(1.被减数转为补码 2.结果补码转为元原码),相当于和直接使用反码计算的结果一样。

补码的出现, 还解决了0的符号以及两个编码的问题:

1-1 = 1 + (-1) = [0000 0001]原 + [1000 0001]原 = [0000 0001]补 + [1111 1111]补 = [0000 0000]补=[0000 0000]原

这样0用[0000 0000]表示, 而以前出现问题的-0则不存在了.而且可以用[1000 0000]表示-128。(但是注意因为实际上是使用以前的-0的补码来表示-128, 所以-128并没有原码和反码表示)

使用补码, 不仅仅修复了0的符号以及存在两个编码的问题, 而且还能够多表示一个最低数。这就是为什么8位二进制, 使用原码或反码表示的范围为[-127, +127], 而使用补码表示的范围为[-128, 127].

四:整形存储计算的原理探究

接下来,就要进入讲解原理阶段,也是最有干货的阶段啦~~

我们使用问题驱动的学习方式,这里的核心问题就是:为什么使用补码可以解决之前提到的两个问题?(1.让符号位也参与运算,简化电路 2.加法和减法统一)

首先,就像上面所说,计算机为了电路的简化,所以设计的电路只能处理加法,而且数据的存储位数是有限的。(比如int为32位,超出就溢出直接截断),所以,可以用时钟作为模型来模仿,顺时针走为加正数,逆时针走为加负数,符号位表示走的方向。

试想一下,一个时钟从格子8要回到格子4,怎么办?没错,顺时针8个格子和逆时针走4个格子。所以顺时针走8个格子的效果是和逆时针走4个格子的效果是一样的。(假设只有分针),从5走到3呢?顺时针10个格子和逆时针2个格子效果也是一样的。看出规律了么?4+8=12,10+2也等于12,12是什么,时钟格子的总数

假如M为12(即时钟的总格子数),则对应的“补码”计算公式就是[x]补=M-|x|。很明显,规律就是逆时针走x格子和顺时针走M-|x|格子效果一样!

以下根据时钟来模拟我们的整数加法,这里规定一旦走的格子超过了一圈,就算为向符号位进位1:

1.正数加正数:
直接原码加原码。符号怎么加都为0,即正数。比如8+4,格子8顺时针走4个格子,结果走到格子12。如果是溢出的情况,比如8+5,格子8顺时针走5格子,走到1,因为超过了12,所以溢出,结果是错误的。

2.正数加负数:
结果为正数:
8+(-4),也就是说,时钟从从格子8逆时针走4个格子,相当于8-4 = 4,但是由于时钟只能顺时针走,所以只能格子8顺时针走8个格子,8+8=16,但是因为时钟只有12个格子,相当于溢出到符号位,所以数值位剩下4,符号位0+1+1 = 0,进位的1溢出截断。所以结果数值位为4,符号位为0。

结果为负数::
假如是8-9,即从格子8逆时针走9格子,只能往前走2格子(加补码),符号位为0+1,结果为11,注意11还是“补码”,需要转为“原码”,根据[x]补=M-|x|算出补码为1,因为符号位为1,所以结果是-1。

3.负数加负数:
假如是-1+(-1):
转化为0+(-1)+(-1)。首先0顺时针走11格子(-1的“补码”),再顺时针走11格子(-1的“补码”),此时走到格子10,符号位为0+1+1 = 2,进位1直接溢出,为0。但是因为刚才已经超出了一圈,所以符号位要加1,所以为负数,转化为原码是-2。

以上模拟了通过补码进行完整的一个整数在计算机中的运算,还原到计算机计算,假如整形用一个字节表示,且只能使用加法,那么整数的计算就是相当一个只能顺时针前进,并且时钟刻度是从0到255的时钟!通过这样巧妙的转化,使得符号位可以参与运算,并且将加减法统一起来了!

好了,以上尽量用通俗的方式解释了整数在计算机中的存储和原理,如果要了解更理论的原理,请参考:原码, 反码, 补码 详解

发布了69 篇原创文章 · 获赞 76 · 访问量 8万+

猜你喜欢

转载自blog.csdn.net/sinat_23092639/article/details/86749992