我们知道日常生活中使用的数分为整数和实数,整数的小数点固定在数的最右边,可以省略不写,而实数的小数点则不固定。在计算机中只能识别和表示“0”和“1”,而无法识别小数点,因此要想使得计算机能够处理日常使用的数据,小数点的问题是不可避免的。

  关于计算机系统中实数的表示,在下篇文章中会讲解。本篇博客我们讲解的是整数在计算机系统中如何表示。  

  在各种大学教材,各种网站论坛中,对于整数编码表示方法的正确打开姿势(姿势要帅)如下:

  

1、机器数

  机器数(computer number)是数字在计算机中的二进制表示形式。机器数有2个特点:

  ①、符号数字化。因为计算机硬件只认识两种物理状态(用0和1表示),因此数据的正负号在机器里就用一位二进制0或者1来区分。在计算机用一个数的最高位存放符号, 0代表符号“+”,以1代表符号“-”。

  ②、机器数的大小受机器字长的限制。机器内部设备一次能表示的二进制位数叫机器的字长,一台机器的字长是固定的。字长8位叫一个字节(Byte),机器字长一般都是字节的整数倍,如字长8位、16位、32位、64位。

  比如在字长为8的计算机中,十进制数+5,其机器数为00000101;十进制数-5,其机器数为10000101。

2、真值

  计算机机器数真正的值称为真值。因为机器数的最高位是符号位,所以我们在计算真值的时候要分区分开。

  比如上面讲的机器数10000101,单纯作为一个二进制数,我们转换为十进制是133。但是其真值是不计算符号位的,其最高位的1表示"-"。所以10000101的真值为-5。

3、机器数的原码、反码、补码三种形式

  前面我们讲过机器数是在计算机中的二进制表示形式,但是在计算机中,这种表现形式又分为原码、反码、补码等三种最常用的形式。

  ps:下面举例都是字长为8。

  ①、原码

原码=符号位+真值

  比如:

  [+5]原码=0 0000101

  [-5]原码=1 0000101

  原码表示与真值对应直观,而且转换也简单。但是用原码进行加减运算的时候,会出现以下问题:

使用原码计算表达式:1 - 1 = 0
1 - 1 = 1 + (-1)= [00000001]原 + [10000001]原 = [10000010]原 = -2

  注意:计算机是没有减法器,只有加法器,减法运算可以转换为加上那个数的负数。

  我们发现通过原码计算1 - 1 表达式结果居然是 -2。所以早期计算机机器数采用原码编码的时候,在进行原码加减运算时,必须先判定是否是两个异号数相加或两个同号数相减,若是,则必须判定两个数的绝对值大小,根据判断结果决定运算结果符号,并用绝对值大的数减去绝对值小的数。也就是说用这样一种形式进行加运算时,负数的符号位不能与其数值部分一道参加运算,而必须利用单独的线路确定符号位。很显然,这样设计电路就很复杂,这是不经济实用的,为了解决这个问题,反码产生了。

  ②、反码

反码:正数的反码与其原码相同;负数的反码是对其原码逐位取反,但符号位除外。

  我们用反码来计算 1 - 1

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

  看上去结果好像是正确的了,但是大家发现没,结果是-0,虽然对于0的符号没有什么实际意义。但是在计算机中,0如果用原码和反码表示会有两种形式:

[+0]=[0000 0000]原=[0000 0000]反
[-0]=[1000 0000]原=[1111 1111]反

  两种编码就两种编码吧,只不过是多占用一个计算机表示数的编码形式。只要结果是正确的,我们还是能够忍受的,然而。。。。

  请用反码计算表达式  2 -1

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

  是不是很奇怪,原码计算 2 - 1 得到的结果居然是 0 。其实稍微分析计算过程我们也知道,再用反码进行加法运算的时候发生了进位,而由于字长为8,进位就直接省略了,便造成了错误。这肯定是不被允许的,所以采用反码的计算机解决办法如下:

  反码的符号位相加后,如果有进位出现,则要把它送回到最低位去相加(循环进位)。

2 -1 = 2 + (-1) = [0000 0010]原 + [1000 0001]原 = [0000 0010]反 + [1111 1110]反+[0000 0001]循环进位 = [0000 0001]反 = [0000 0001]原 = +1
13 - 6 = 13 + (-6)= [0000 1101]原 + [1000 0110]原 = [0000 1101]反 +[1111 1001]反+[0000 0001]循环进位=[0000 0111]反 = [0000 0111]原 =+7

  采用反码运算虽然较好的解决了原码运算所遇到的困难或问题,但由于循环进位需要二次算术相加,延长了计算时间,这同样给电路带来麻烦。这时候补码登场了。

  ③、补码

补码:正数的补码与原码相同,负数的补码等于其反码的末位加1

  我们来看下面这个例子:

  2 - 1 = 1

2 -1 = 2 + (-1) = [0000 0010]原 + [1000 0001]原 = [0000 0010]反 + [1111 1110]反 = [0000 0010]补 + [1111 1111]补 = [0000 0001]补 = [0000 0001]原 =+1

  9 + 12 = 21

9 + 12 = [0000 1001]原 + [0000 1100]原 = [0000 1001]补 + [0000 1100]补 = [0001 0101]补 = [0001 0101]原 = 21

  

  我们发现补码运算就很简单了,产生的进位直接舍去,而且不做多余的操作也解决了进位的问题。还有 +0 和 -0 的表示,在原码和反码都有两种形式,但是补码却只有一种:

[+0]=[0000 0000]原=[0000 0000]反=[0000 0000]补
[-0]=[1000 0000]原=[1111 1111]反=[0000 0000]补

  

  就这样我们完美的解决了计算机中整数运算的问题。计算机的机器数采用补码的形式,我们在做算术运算的时候,既不需要额外的判断,又能得到准确的结果。

  看上去本文应该结束了,然而......

  请求出 127+1 的值

4、溢出

  接着上面抛出的问题,127+1的值,我们现在程序中看看:

public static void main(String[] args) {
byte x = 127;
byte y = 1;
byte k = (byte) (x+y);
System.out.println(k); //-128
System.out.println(Byte.MIN_VALUE+"~"+Byte.MAX_VALUE); //-128~127
}

  byte在计算机正好是一个字节,也就是8位二进制序列。我们发现127+1结果不是128,反而是-128,这就是结果发生了溢出。因为byte表示数的范围是-128-127,128超出了这个范围。用补码计算如下:

1 + 127 = [0000 0001]原 + [0111 1111]原 = [0000 0001]补 + [0111 1111]补 = [1000 0000]补

  我们发现这个数的符号位没有发生进位,但是数值最高位发生了进位。在看前面的2-1

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

  这个表达式符号位和数值最高位发生了进位,但是结果却是正确的。总结如下:

  只有一个高位进位或者符号位进位就为溢出的规则。

  溢出是每种编码在运算时都不可避免的,一般来讲结果超过字长所表示数的范围都会发生溢出。而判断机器是正常进位还是溢出的基本依据,在微型机中可用异或电路来实现上述的判断。在实际编码中解决办法也很简单,就是将结果用更大范围的编码形式接收即可。比如两个byte类型的数相加,我们用 int 来接收即可。

public static void main(String[] args) {
byte x = 127;
byte y = 1;
int k = x+y;
System.out.println(k); //128
System.out.println(Byte.MIN_VALUE+"~"+Byte.MAX_VALUE); //-128~127
}

  所以我们可以说用补码进行运算,在不考虑溢出的情况下,结果都是正确的。确实也是这样,但是......

  请求出 -128 的补码?

5、剧情反转

  上面的给出的问题,-128 的补码,我们首先想到去求它的原码,嗯,原码应该是 [1000 0000],不对,第一位不是符号位吗,[1000 0000]应该表示 -0。那应该怎么用原码表示 -128呢,我们发现字长为 8 的计算机用原码是无法表示的,反码也是一样。我们看看补码,用 -127- 1 的表达式结果来计算 -128 的补码:

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

  -128的补码形式为 [1000 0000],我们能通过算术表达式得到某个数的补码形式,但是为什么直接就求不出来?那么计算机自己是怎么实现的呢?

  再来看这样一个问题:我们日常使用的钟表,比如现在钟表指向的是 10点钟,我要将钟表调整到 6 点钟,则有两种拨法:

  ①、顺时针将时针拨动 8 格

  ②、逆时针将时针拨动 4 (12-8) 格

  由此给大家普及一个概念叫 “模”,钟表便是一个典型的模运算系统,其模数为12。

  同理,对于十进制两位数,在将结果百位舍掉的情况下,50可以用60-10得到,或者60+90得到。这里的90也就是100-10得来的,那么我们就说十进制两位数运算系统的模数为100。

  我们判定:两个相加等于模的数互为补数。

  在模表示的范围内做减法运算,可以将“X-Y”的减法变更为“X+Y的补数“的加法,当然这里不考虑结果溢出。

  上面我们举的例子都是大数减小数,如果是小数减大数会怎样?

  如果是10-80,结果应该是-70。但是如果按照 10+(100-80)的说法,结果是30。很明显,30和-70不是同一个结果,而且也没有产生百位进位。那我们应该怎么办呢?

  解决办法很简单,就是让这两个数相等,而且这正好解决了负数的表示方法,-70的绝对值的补数正好是30。

  但是问题又来了,这里的30已经表示正数30了,现在又表示负数-70,那我们怎么知道它到底表示哪个数?

  为了解决这个问题,我们给这套规则规定一个范围,原来是0~99的正数,现在既然要用部分正数来代替负数了,那就要规定一个范围来使得一个数只代表一个含义,正好一人一半,0~49这个区间就代表正数,50~99的区间就用来代表各自补数的负值,例:98就代表-2

  所以0-99的编码数可以表示的数的范围为 -50-49。

  我们解决了十进制两位数的减法运算,那么在字长为 8 的计算机系统中,我们又该如何呢?

  8位二进制数可以表示的数为2的8次方,0-255,一共 256 个数,0也要占用一位数。所以我们说 256 是8 位二进制数的模,这和上面说的十进制两位数0-99,模为100是一样的。

  我们按照前面讲的逻辑,一半的数0-127,代表其正数本身,另一半的数128-255表示其补数的负值,即-1~-128。

  所以而 “X-Y”的减法 就用 “X+Y的补数” 的加法来表示,即将减法的形式转换为加法的形式了,而且计算结果还是正确的。

  注意:这里还是一样,不考虑结果的溢出,也就是计算值和结算结果都必须在-128~127之间,一旦超过这个范围,结果就不准了,这也是程序员日常编码说的int=int+int,如果结果大于int类型表示的范围,那得出来的结果肯定不准。

  由此我们得出来的结论是:

  计算机编码其实并没有什么所谓的符号位,但是由于计算机没有减法运算,为了将负数变为某个可以运算的编码来进行加法运算,补码产生了。这也间接说明了正数的补码是不变的,而负数的解决办法是首位不变,其余的取反再加1。

  我们上面说的补码怎么得来的,就是 模-绝对值 。

  所以这个时候我说让你求 -128的补码,你马上就会知

  256 - |-128|=128  而128的二进制补码是不是就是 [1 0 0 0 0 0 0 0]

  让你求 -1 的补码,你马上就会知

  256 - |-1| = 255,其255的二进制补码形式就是[1111 1111]

  注意:关于这样求补码的具体数学证明,请参考《计算机组成与系统结构》。

  

6、总结

  本篇文章你可以直接从第 5 点开始看,忘掉计算机编码的什么首位符号位,忘掉计算机补码是由原码取反加1,回归简单直白的理解。计算机是机器,硬件能理解的只有高低电平,也就是0或者1。它知道什么是符号位吗?这些规则只不过是为了更好的完成减法运算所yy出来的。

  大学也学过这些编码方式,但是都是背书式记忆,希望这篇文章能给大家带来一些帮助。

  个人见解,如有错误欢迎大家抛砖!!!

  

参考书籍:《计算机组成与系统结构》

参考文章: https://www.zhihu.com/question/20458542?sort=created

原码、反码、补码的正(nao)确(can)打开方式的更多相关文章

  1. Java基础-原码反码补码

    Java基础-原码反码补码 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 注意,我们这里举列的原码和反码只是为了求负数的补码,在计算机中没有原码,反码的存在,只有补码. 一.原码 ...

  2. 大数据学习--day02(标识符、变量、数据类型、类型转换、进制转换、原码反码补码)

    标识符.变量.数据类型.类型转换.进制转换.原码反码补码 标识符: java50个关键字不能做标识符,以数字开头不能做标识符(这个老是忘记写一个类名的时候) 变量: 变量分为成员变量和局部变量,注意作 ...

  3. JAVA:二进制(原码 反码 补码),位运算,移位运算,约瑟夫问题(5)

    一.二进制,位运算,移位运算 1.二进制 对于原码, 反码, 补码而言, 需要注意以下几点: (1).Java中没有无符号数, 换言之, Java中的数都是有符号的; (2).二进制的最高位是符号位, ...

  4. 原码 & 反码 & 补码 & 详解

    本篇文章讲解了计算机的原码, 反码和补码. 并且进行了深入探求了为何要使用反码和补码, 以及更进一步的论证了为何可以用反码, 补码的加法计算原码的减法. 论证部分如有不对的地方请各位牛人帮忙指正! 希 ...

  5. Java学习第五篇:二进制(原码 反码 补码),位运算,移位运算,约瑟夫问题

    一.二进制,位运算,移位运算 1.二进制 对于原码, 反码, 补码而言, 需要注意以下几点: (1).Java中没有无符号数, 换言之, Java中的数都是有符号的; (2).二进制的最高位是符号位, ...

  6. C语言原码反码补码与位运算.

      目录:     一.机器数和真值     二.原码,反码和补码的基础概念     三.为什么要使用原码,反码和补码     四.原码,补码,反码再深入     五.数据溢出测试     六.位运算 ...

  7. python之计算机硬件基本认知_数据单位_进制间转换_数的原码反码补码

    一:计算机硬件基本认知 cpu:   中央处理器.   相当于人的大脑.运算中心,控制中心. 内存:  临时存储数据. 优点:读取速度快,缺点:容量小,造价高,断电即消失. 硬盘:  长期存储数据. ...

  8. C 标识符, 数据存储形式(原码,反码,补码)

    一.  标识符 第一个字母必须是英文字母或下划线 二. 数据存储形式(补码存储) 最高位是符号位 ---- 0表示整数 ; 1 表示负数 1. 正数:原码 = 反码 = 补码 例子 : (10) 原码 ...

  9. java基础知识-原码,反码,补码

    1.正数:原码,反码,补码:都一样. 2.负数:和正数的储存方式不同,负数都是以补码形式存储的. <1>负数的补码 把负数的原码除了符号位取反后再+1. <2>负数的原码 把对 ...

随机推荐

  1. commons-pool与commons-pool2连接池

    commons-pool和commons-pool2是用来建立对象池的框架,提供了一些将对象池化必须要实现的接口和一些默认动作.对象池化之后可以通过pool的概念去管理其生命周期,例如对象的创建,使用 ...

  2. opencv-python:win7下,搭建python2.7.5环境,配置opencv3.1.0准备开工-OpenCV步步精深

    我的个人博客:点这里 搭建python2.7.5环境 下载python2.7.5 64位:https://www.python.org/ftp/python/2.7.5/python-2.7.5.am ...

  3. 4.ElasticSearch的基本api操作

    1. ElasticSearch的Index 1. 索引初始化 在创建索引之前 对索引进行初始化操作 指定shards数量和replicas数量 curl -XPUT 'http://192.168. ...

  4. 开始Java8之旅(四) --四大函数接口

    前言   Java8中函数接口有很多,大概有几十个吧,具体究竟是多少我也数不清,所以一开始看的时候感觉一脸懵逼,不过其实根本没那么复杂,毕竟不应该也没必要把一个东西设计的很复杂. 几个单词   在学习 ...

  5. LeetCode 74. Search a 2D Matrix(搜索二维矩阵)

    Write an efficient algorithm that searches for a value in an m x n matrix. This matrix has the follo ...

  6. nvm进行node多版本管理

    写在前面 nvm(nodejs version manager)是nodejs的管理工具,如果你需要快速更新node版本,并且不覆盖之前的版本:或者想要在不同的node版本之间进行切换: 使用nvm来 ...

  7. 解决 iframe 在 ios 上不能滚动的问题

    HTML代码在使用IFRAME或者其他HTML元素时,你需要使用一个元素(如DIV)来包装他们: <div class="scroll-wrapper">  <i ...

  8. JS框架设计读书笔记之-选择器引擎02

    选择器引擎涉及相关概念 概念 以Sizzle的主函数声明为例,来说明引擎的相关概念. function Sizzle(selector, context, results, seed) { //... ...

  9. SE6 不定参数和默认参数详解和使用细节

    在SE5以前我们通常通过arguments类数组对象来引用不定形参,SE6则使用了一种叫做不定参数的写法,比起隐式的arguments要直观的多. 不定参数使用...参数名来指定一个不定参数,参数名指 ...

  10. Javascript中的Trait与代码重用

    Javascript中的Trait与代码重用 来源 http://www.ituring.com.cn/article/64103 我们知道,OOP中最普遍的代码重用方式是通过继承,但是,继承有一些缺 ...