ToB企服应用市场:ToB评测及商务社交产业平台

标题: 关于二进制的原码、补码和反码,以及表示范围、常见位运算符和进制转换的理 [打印本页]

作者: 光之使者    时间: 2024-4-3 02:48
标题: 关于二进制的原码、补码和反码,以及表示范围、常见位运算符和进制转换的理
【版权声明】未经博主同意,谢绝转载!(请尊重原创,博主保留追究权)
https://www.cnblogs.com/cnb-yuchen/p/17963363
出自【进步*于辰的博客
参考笔记一,P3.13、P5.1;笔记三,P43.1/3、P44.1。
注:我暂且没有整理关于二进制、原码、补码和反码等概念的理论,本文中的阐述都基于我对相应概念的理解,推荐两篇博文(转发):
这两篇文章都是对我的启发之作,一些概念(如:机器数)也出自于此,建议大家先去浏览这两篇博文,这样会更便于阅读本文。

目录

1、二进制相关概念

在研究8位二进制的表示范围之前,需要先了解原码、反码和补码这三个概念。
1.1 原码、反码

“原码”指符号位加上“真值"绝对值的“机器数”。
反码定义:(摘录自第一篇启发之作)
正数的反码与其原码相同;负数的反码是对其原码逐位取反,但符号位除外。
1.2 补码

先说说补码定义:
正数的补码是其本身的原码;负数的补码是其反码+1后的结果。
补码是做什么的?(摘录自第一篇启发之作)
引入补码是为了解决计算机中数的表示和数的运算问题,使用补码,可以将符号位和数值域统一处理,即引用了模运算在数理上对符号位的自动处理,利用模的自动丢弃实现了符号位的自然处理,仅仅通过编码的改变就可以在不更改机器物理架构的基础上完成预期的要求。
我用几个示例简单说明一下我这段阐述的理解。
示例1:1 + (-2) = -1
看下述示例。
  1. 1 + (-2) = 0000 0001 + 1000 0010 = 1000 0011,得:-3
复制代码
结果显然不对,是因为没有考虑到符号位,符号位不能直接参与运算。
那若是考虑符号位应如何?看示例。
  1. 由于-2的绝对值大于1,故:
  2. 1 + (-2) = -(2 - 1) = 1(000 0010 - 000 0001) = 1000 0001,得:-1
复制代码
这显然增加了硬件的开销和复杂性。(本人对硬件了解有限,所以暂且不知此结论的出处)
若引入补码:
  1. 求-2的补码:-2的原码1000 0010 → 反码1111 1101 → 补码1111 1110
  2. 求1的补码:1是正数,故就是其原码0000 0001
  3. 计算:1 + (-2) = 0000 0001 + 1111 1110 = 1111 1111
  4. 由于都是补码,所以1111 1111也是补码,
  5. 1111 1111减1 → 反码1111 1110 → 原码1000 0001,真值为:-1
复制代码
从此示例可看出,补码将符号位和数值域统一处理。换言之,不需要考虑正负大小情况。
示例2
1、1 + 256 = 1。
256是8位无符号二进制的,其机器数为1 0000 0000。这是如何得出的?
8位无符号二进制的最大机器数是1111 1111,进行+1,得1 0000 0000,但只有8位,最高位1自然丢失。得00000000。因此,1 + 256 = 1 + 0 = 1。
这就是上文所述的“模的自动丢弃实现了符号位的自然处理”。
2、1 + 128 = 1。
128是8位有符号二进制的,其机器数为1000 0000(理论上)。这又是怎么个说法?
先透露:8位有符号二进制无法表示128。
无妨,可暂且将其视为无符号二进制进行计算。128 = 127 + 1,127的补码是0111 1111,进行+1,得1000 0000。
那么,1 + 128 = 0000 0001 + 1000 0000,得1000 0001。不过,我们只是将其视为无符号,实际有符号,故符号位无效,不进行计算。因此,结果为0000 0001,真值为1。
注意:在计算机底层,数都是以补码的形式进行表示。
扩展一点:
在查阅有关补码的资料时,我注意到一些文章中阐述的负数补码的另一种计算方法:
  1. 绝对值原码 → 取反 → +1
复制代码
综本文所述,补码的计算方法:
  1. 原码 → 反码 → +1
复制代码
另一种方法可行吗?
经过观察,结论是:当然可行。
负数绝对值原码与负数原码的不同唯有符号位,如:1 = 0000 0001、-1 = 1000 0001,故负数绝对值原码取反与负数反码相同。
因此,两种方法都可行,大家觉得怎么方便怎么来。
2、八位二进制的表示范围

1、无符号二进制。
既然“无符号”,则无正负之分,都表示正数。
因此,8位无符号二进制的表示范围是0 ~ 255。
2、有符号二进制。
关于8位有符号二进制的表示范围,其中细节比较复杂,暂不讨论,我暂且简述,详述可查阅第一篇启发博文。
分析:
8位二进制的范围是0000 0000 ~ 1111 1111。
1000 0000 = 1000 0001 - 1(无视符号位,这样等式才成立),可实际上都表示负数,则是+1。1000 0001是-1,故1000 0000是0。
可0明明是0000 0000?那么,可视为+0,则1000 0000是-0。
从上文【示例2】可知,8位有符号二进制的是128。因此,-0等同于-128。所以,1000 0000的真值为-128。
补充说明
上述推导基于原码,当然也可以使用补码,只是原码更易于理解。
如下:
1000 0000 = 1000 0001 - 0000 0001(由于都是补码,故等式也成立),1000 0001是-127,则1000 0000是-128。
为什么是补码,等式就成立?
“引入补码”的目的之一是为了解决数的运算问题,比如:-1 + 2,用原码运算时就要考虑绝对值的大小问题。引入补码后就无需考虑,故可以说“负数的补码是正数”(当然负数的补码还是负数,只是视为正数)。
因此,8位有符号二进制的表示范围是-128 ~ 127。
3、常见位运算符

~>>>&|^取反左移有符号右移无符号右移按位与按位或按位异或先说明:
3.1 取反 ~

运算规则:逐位反转,即:0 → 1, 1 → 0。
示例:计算~23。
  1. 23的补码:0001 0111
  2. ~23 = 1110 1000
  3. 1110 1000 → 反码1110 0111 → 原码1001 1000,真值为-24
复制代码
扩展一点:
在运算规则上,取反~与反码相同。不过,~不考虑符号位,与反码定义不同。
3.2 左移  1 = 0000 1011,真值为11[/code]2、计算 -23 >> 1。
  1. 23的补码:0001 0111
  2. 23 << 1 = 0010 1110,真值为46
复制代码
3.4 无符号右移 >>>

也称之为“逻辑右移操作符”。
运算规则:去低位,补高位,无论正负,都补0。
示例:
1、计算23 >>> 1。
  1. 计算-23的补码:-23原码1001 0111 → 反码1110 1000 → 1110 1001
  2. -23 << 1 = 1101 0010
  3. 11010010 → 反码1101 0001 → 原码1010 1110,真值为-46
复制代码
2、计算-23 >>> 1。
  1. 23的补码:0001 0111
  2. 23 >> 1 = 0000 1011,真值为11
复制代码
从上文可知,负数补码的第一个1位前都是1。
3.5 按位与 &

运算规则;当对应二进制位同为1时,得1。(不考虑正负)
示例:
1、计算7 & 23。
  1. -23的补码:1110 1001
  2. -23 >> 1 = 1111 0100
  3. 1111 0100 → 反码1111 0011 → 原码1000 1100,真值为-12
复制代码
2、计算7 & -23。
  1. 23的补码:0001 0111
  2. 23 >>> 1 = 0000 1011,真值为11
复制代码
3.6 按位或 |

运算规则:当对应二进制位有一个为1时,得1。(不考虑正负)
示例:
1、计算7 | 23。
  1. -23的补码:11111111 11111111 11111111 11101001
  2. -23 >>> 1 = 01111111 11111111 11111111 11110100,
  3. 真值为Integer.MAX_VALUE - 11 = 2147483636
复制代码
2、计算7 | -23。
  1. 7的补码:0000 0111
  2. 23的补码:0001 0111
  3. 7 & 2 = 0000 0111,真值为7
复制代码
3.7 按位异或 ^

运算规则:当对应二进制位相同时得0,否则得1、(不考虑正负)
示例:
1、计算7 ^ 23。
  1. 7的补码:0000 0111
  2. -23的补码:1110 1001
  3. 7 & -23 = 0000 0001,真值为1
复制代码
2、计算7 ^ -23。
  1. 7的补码:0000 0111
  2. 23的补码:0001 0111
  3. 7 | 23 = 0001 0111,真值为23
复制代码
3.8 扩展

<blockquote>我在看Java-API源码时,遇到一些奇怪的位运算符,如: 1 = 0000 1011→ 23 + 21 + 20 = (24 + 22 + 21 + 20)/2。
——————
当然,这个等式是不成立的,因为等号右边多了个 20,即多了1。不过,大家肯定已经看出来了我这么写的用意。
23是奇数,它的二进制是0001 0111,最低位是1。右移1位,这个 1/20 就没了,等式成立;而其他位都变为原来的1/2。因此,总和也变成原来的1/2。
3、-11 >> 1 = -6。
  1. 7的补码:0000 0111
  2. -23的补码:1110 1001
  3. 7 | -23 = 1110 1111,真值为-7
复制代码
-11是奇数,同样先-1,再取1/2。
4、-6 / 1 = 1000 0101,真值为-5        // 注意:这里右移补的是0-5 > 1 = -5、-15 >> 1 = -7,两个结果显然不对。当然,我还做了其他测试,负奇数和负偶数都有,结果有正确的也有错误的。
由于这只是我发现的一个规律,并没有理论支撑,所以未列举出来进行说明。不过,似乎负偶数使用此技巧运算的结果是对的(也算是一个规律吧。。。)。
因此,大家对这个技巧有个印象就行,实际计算还是要严谨。
结语:
为什么说上文这个性质巧妙?
因为我发现在很多源码中都采用这种方法进行数值翻倍或取半,特别是一些包装类(java.lang.*)或工具类(java.util.*)。
例如:
这是java.util.ArrayList类的扩容方法,很经典的例子,方法详情见源博文的第5.6项。
4.2 实现字符大小写转换

char 类型对应ASCLL码,对字符进行-/+ 32运算即可实现大小写转换。
在查阅关于位运算的资料时,我发现通过位运算也可以实现字符大小写转换。由于位运算的对象是二进制,故效率优于算术运算。好奇测试一下发现,如果都运算一亿次,时间差在几十甚至几微秒之间,实际差距微乎其微。因此,我将此方法记录下来的主要目的是为了“扩展思维”。
看下述示例。
1、'A' ^= 32 → 'a'。
  1. 7的补码:0000 0111
  2. 23的补码:0001 0111
  3. 7 ^ 23 = 0001 0000,真值为16
复制代码
2、'a' ^= 32 → 'A'。
  1. 7的补码:0000 0111
  2. -23的补码:1110 1001
  3. 7 ^ -23 = 1110 1110,真值为-18
复制代码
3、'A' |= 32 → 'a'。
  1. 计算-11的补码:-11原码1000 1011 → 反码1111 0100 → 补码1111 0101
  2. -11 >> 1 = 1111 1010
  3. 1111 1010 → 反码1111 1001 → 原码1000 0110,真值为-6
复制代码
4、'a' &= -33 → 'A'。
  1. 计算-6的补码:-6原码1000 0110 → 反码1111 1001 → 补码1111 1010
  2. -6 << 1 = 1111 0100
  3. 1111 0100 → 反码1111 0011 → 原码1000 1100,真值为-12
复制代码
4.3 生成 IP 地址

大家看一个例子就明白了,看这里 → Inet4Address类的第2.2项,这里就不再赘述。
5、关于进制间转换

最后

本文中的例子为了阐述这七种常见位运算符的运算步骤、方便大家理解而简单举例的,23与-23是任意取的数,没有特别意义。
PS:
单纯的位运算最大的作用就是帮助我们掌握位运算的基础,没有太大实用价值。大家可以偶尔去看看一些源码中对位运算的运用,很多真的很巧妙,而且还能查漏补缺。
本文能有如此规模,得益于我对二进制、位运算的理解,以及平日解析源码时频繁运用位运算。
本文完结。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!




欢迎光临 ToB企服应用市场:ToB评测及商务社交产业平台 (https://dis.qidao123.com/) Powered by Discuz! X3.4