从JavaScript的移位运算看数字在计算机内部的编码——补码
偶然看到一个JavaScript的题目:
js中13>>2=? -13>>2=?
在浏览器中很容易测试出答案分别是 3 和 -4。
13>>2 = 3 很好理解,但是对于 -13>>2 = -4 ,我无法理解。然后我又去用 Java 语言实现一遍,结果也是一样的。
我知道关于 “原码、反码、补码” 这个知识点是 《计算机组成原理》 中的内容,但苦于网上下载不到该书,只得去Google各种博客去深入了解。
(吐槽下,Google搜索“原码 反码 补码”的结果,比度娘搜索该关键词的结果好太多!用百度真真是浪费生命。)
看着充满数学公式,和无聊的 “计算方法”,都是些教材上的理论东西,越看越迷糊。后来,我就总结了:
原码、反码、补码
其实,抛开应付《计算机组成原理》考试,以后(无论笔试、面试、Coding)遇到的都是 “补码”,没人关心 原码 和 反码 是个什么。
为什么呢? 因为 在计算机内部(寄存器层面),数都是以补码的形式存储在寄存器中的。
原码,反码什么的都是为了计算补码存在于人类思维中的,可无视它们。
比如,此题中,13 是原始数,移位移的是 13 的补码(下面细讲JavaScript整数的补码),移位后再将 补码 反算成 原始数。
补码的简便计算方法
无论C/C++/Java强类型语言,还是JavaScript,都有移位运算符,在CPU中的运算器进行移位操作时,这个过程都是一样的。
这里为了方便,假设 在一个寄存器只有8位的机器上,整型只能用 8个比特位来表示。本文讲的都是有符号数。
那么,
13的 补码 就可以表示为:(其中D 表示 十进制数, B 表示二进制数)
13 (D) = 0000 1101 (B)
-13 的 补码 就是:
-13 (D) = 1101 0011 (B)
那么 -13 的补码是怎么算出来的呢? 很简单,只要 让 -13 的补码 加上 13 的补码, 溢出后变成 0 就行了。
这有什么根据么? 当然有根据了,我们知道为什么发明 补码么? 就是因为 原码 和 反码 会出现 +0 和 -0 ,
所以才不为计算机科学家采用。而接着发明出来的补码,恰好补上这一漏洞,使得计算机可以和正常的数学运算
一样,完美的完成加减乘除。
所以,如果 在数学里,
13 + (-13) = 0
那么,在计算机的世界里,必须
0000 1101 + 1101 0011 = 0
因为造出补码的意义就是为了让计算机内补码和数学中的数字一一对应,就是能完全代表数学中的数。这样才能使用传承了
几千年的数学知识进行更高深的微积分等科学计算。
这里举一个简单的例子。
一台8位寄存器的机器,13 的补码 是 0000 1101 ,那 -13 的补码呢?
根据 -13 + 13 = 0 ,可以这么算:
0000 1101
+ xxxx xxxx
———————
1 0000 0000 (8寄存器,进位的1溢出被丢弃。寄存器中就保存的是 0000 0000 ,就是数学中的0)
这样,很容易凑出 -13 的补码,再简单不过了。就只有1和0进行加减。-13的补码可以算出 是 1111 0011。
比之,教科书式的
(这么头大的公式,你会去用,这明显是数学家们为了论述严密而显摆给学生们看的)
和一般教学中用的:
负数的补码,是反码加1。而反码又是原码,符号位不变,加1。
(差不多,只是多了个抽象概念。反码没有必要记忆,时代遗留产物,大胆抛弃)
>> << 移位运算
算出了 13 和 -13 的补码,接下来就是 右移 2 位了。
无论什么语言,只要是在X86的机器,其移位运算在汇编层都是通过 算术右移指令SAR(shift arithmetic right)执行的。而算术右移指令的
具体操作是,将寄存器中的数值(就是1和0的数串)右移,最左端用最高位填充,而不是补零。这是X86汇编规定的,至于为什么有些原因,不赘述。
那么, 13 的补码右移 2 位后是:
13的补码: 13 (D) = 0000 1101 (B)
13的补码右移2位后: 0000 0011 (最右边的01被丢掉)
-13的补码: -13 (D) = 1111 0011 (B)
-13的补码右移2位后: 1111 1100 (最右边的11被丢掉)
移位后反算出数值
移位完了,并不代表结束了。移位后的数字还在寄存器中,是未知数(我们要求的未知数)的补码的数串。
然后,我们要根据补码,算出该数来。
依据《计算机组成原理》:正数是原码、反码、补码三码合一。
13 的补码右移2位后的数串(未知数的补码形式),换算成十进制,就是 3。
13 移位后的补码容易换成十进制(因为是正数,所以很容易算出来),而 -13 的移位后的补码究竟是十进制的多少,就要小算一下。
用上面的方法你能算出来吗?
计算过程:
-13的补码右移2位后: 1111 1100
+ xxxx xxxx
———————
1 0000 0000
凑出 一个加数来,应该很简单吧。第二个加数xxxx xxxx就是 0000 0100 ,就是 十进制的 4。
就是说 1111 1100 是 -4 , -13移位后的补码是 十进制的 -4 。
答案就出来了。 13 >> 2 等于 3, -13 >> 等于 -4.
再说JavaScript
前面说的是 8位 的机器,数字都是用8个比特位来表示。再出现C/C++/Java等高级语言后,数据类型的宽度由语言本身决定,如C/C++的int/long由具体实现语言标准的编译器决定每个数据类型到底需要多少个比特位,而Java则因为有JVM的存在,整型统一为32位。
那在JavaScript中,这个整数到底是用多少个比特位表示呢?。JavaScritp 是这样的:
根据这个问答中有人讲:
在
Javascript权威指南 第六版 3.1小节有讲到:
Number类型统一按浮点数处理,64位存储,整数是按最大54位来算最大最小数的,否则会丧失精度;某些操作(如数组索引还有位操作)是按32位处理的~~
浮点数范围as large as±1.7976931348623157×10的308次方
as small as±5×10的−324次方精确整数范围:
TheJavaScript number format allows you to exactly represent all integers between −9007199254740992and9007199254740992(即正负2的53次方)
数组索引还有位操作:
正负2的31次方
鉴于大家可能没书,下载PDF也费事,就贴出图片来。
根据图片,32位整数,就是说题中的 13 在JavaScript 中是如下表示的:
13 = 0000 0000 0000 0000 0000 0000 0000 1101 (13的补码)
字节编号 1 2 3 4 5 6 7 8
-13 = 1111 1111 1111 1111 1111 1111 1111 0011 (-13的补码)
当然,也有另外一种简单计算补码的方法:
负数的补码这么记简单。
符号位不变。其他的从低位开始,指导遇见第一个1之前,什么都不变。遇见第一个1后保留这个1,以后按位取反。
例:[-7]原= 1 0000111 B
[-7]补= 1 1111001 B
技巧性很强,千万别记错了。
反正我的方法就是:记住正数和负数的补码加起来也是0就对了。(而正数是三码合一)
另附一个有趣小问题:
你知道下面的C代码会出现什么问题吗?(注意移位操作)
1 #include <stdlib.h> 2 #include <stdio.h> 3 4 5 static void divide_by_two(int num) 6 { 7 while (num) { 8 printf("%d\n", num); 9 num = num>>1; 10 } 11 } 12 13 int main() 14 { 15 int num; 16 scanf("%d", &num); 17 18 divide_by_two(num); 19 20 return 0; 21 }
答案见:http://blog.chinaunix.net/uid-23629988-id-3018793.html
PS:在JS中,位运算基本没用。这是强类型语言C/C++/Java才经常用到的内容。
.