大概在06年,我看到了一本书,叫做<<深入Java虚拟机>>。
在周志明那本神书《深入理解Java虚拟机》出来之前,这本书应该是唯一一本讲JVM的书, 对Java class文件格式,执行引擎讲得特别详细。
我看了一遍看完了以后就“热血沸腾”:原来Java 的class 文件格式是这样的啊!也许我也可以写一个JVM了!
于是我就开始琢磨, 先写程序去解析.class文件, 然后写一个小的执行引擎,去执行那些字节码。
作为第一步,我要写个小程序去读取硬盘上的.class文件,然后看看它的头四个字节是不是著名的魔数:“CAFE BABE”
第一个字节实际的输出结果让我大跌眼镜: -54 !
这是怎么回事呢?说好的CA呢?
这个问题让我想了很久,后来终于想明白了,CA 是16进制字符,变成二进制就是 1100 1010 , Java内部使用补码表示数字的,把1100 1010看作二进制补码,它对应的十进制可不就是-54嘛!
原来都是补码惹的祸!
那计算机为什么用补码呢?一个重要的原因就是简化电路的设计:把整数的加减法统统变成加法来运算。
比如一个4位的计算机,能表示的数字是 0 ~ 15
在做加法的时候非常简单:
8+3(十进制) = 1000 + 0011 = 1011 = 11 (十进制)
我们可以设计一套简单的数字电路(与门,或门等)来实现这个加法运算。
但是减法怎么办呢?难道再设计另外一套电路?这就浪费了。
于是人们就引入一个‘补数 '的概念, 例如 3的补数 是 13, 4的补数是12, 5 的补数是11......
当你计算7减去3 的时候, 可以变成 7加上3 的补数, 即 7 + 13 :
7-3 = 0111- 0011 (二进制3) = 0111 + 1101(二进制13) = 10100
10101已经是5位了,溢出了, 去掉最高位是 0100 ,就是十进制4 了。
那二进制的“补数”怎么得出呢?人们想出了一个异常简单, 又特别适合计算机电路的算法, 对二进制数的所有位取反, 然后加1:
3 -> 0011 -> 1100 (取反) -> 1101 (加1)
这种方法是不是很神奇?只用一套加法电路和补码电路就可以高效地实现加法和减法了。
等等,负数怎么办?我们手写的时候,可以在一个数前加个负号, 就可以表示,但是对计算机来说,它必须得用某一位来表达正负,比如用这种方法:
最高位的0 表示整数,1 表示负数。
真正有效的数字只剩下3位了, 正数的范围是从1 到7 ,负数的范围从 -1到-7 ,不过这里出现了两个零!一个正0 , 一个负0 , 这肯定不好!
改进一下,把那个负0 认为是-8吧,这样还能多表示一个数字!
这样,数字的范围变成了从[-8 ,7] 。
推广一下,在编程语言中,对于n位整数,它的取值范围是[-2^(n-1) ~ 2^(n-1) - 1] ,正数要比负数少一个。
但是之前的减法变加法的规则还能用吗?我们试试:
7-4 = 7+(-4) = 0111 +1100 = 10011 -> 0011 (舍掉最高位) = 3, 正确!
4-7 = 4+(-7) = 0100 + 1001 = 1101 = -3 ,正确!
注意,用这种办法,连符号位都参与了运算。
总结一下, 在计算机内部,是使用补码来表示二进制数, 如果是一个正数, 补码就是它本身, 如果是个负数, 需要把除了符号位之外的二进制数进行取反加一的操作。
回到我们开头的问题, 如何正确地把第一个字节变成16进制字符串“CA”,然后展示出来呢?用这个函数就可以了:
后记:本文首发于我的知识星球“码农翻身”的“硬技能”专栏,在那里我主要讲“组成原理”,“编程语言”,“操作系统”,“计算机网络”,“数据库”,这些都是让你起飞的计算机基础知识。
不仅如此,我也邀请好友海飞开了一个“软技能”专栏,叫做“非说不可”,海飞是IBM资深经理,有15年以上工作经验。他也是极客时间《面试现场》专栏作者,对于“面试”,“领导力”,“学习成长”,“个人管理”等软技能方面可以说是信手拈来。
给大家看看已经发过的文章:
你的综合能力 =(硬技能) X (软技能)
欢迎加入我们的知识星球“码农翻身”!你可以向我和海飞任意提问, 我们知无不言,言无不尽,这两个工作经验都在15年+的老家伙,分享宝贵经验,不敢说让你有多大的提升,一定会让你少走几年弯路。
星球原价149元, 使用下面的优惠券,仅需99元即可加入,超值!
优惠券仅限100人,需要的要抓紧了!