反码与补码
反码与补码
课程:Java 语言程序设计
教师:王建民
班级:信2505-2 学号:20253909 姓名:刘家瑞
一、一句话本质
计算机里只有加法器,没有减法器。所有减法都变成「加一个负数」。
反码、补码这一整套东西,存在的唯一目的就是:让减法能用加法实现,并且 +0 和 -0 不打架。
下面用 8 位(byte)举例。
二、原码:最直观,但有两个毛病
原码 = 最高位当符号位(0 正 1 负),剩下 7 位存绝对值。
+5 → 0000 0101
-5 → 1000 0101
毛病很明显:
- 0 有两种写法。
0000 0000和1000 0000都表示 0,机器判断"是不是 0"要判两次。 - 加减法不通用。
+5 + (-5)用原码直接相加是错的:00000101 + 10000101 = 10001010 = -10。
三、反码:过渡品,专为补码铺路
规则:正数不变,负数把原码的数值位按位取反(符号位不动)。
+5 → 0000 0101
-5 → 1111 1010
反码本身仍然没解决 +0 / -0 的问题(1111 1111 是 -0),但它有个关键性质:反码相加,结果回卷(最高位进位加回最低位)后数值是对的。这一步正是理解补码"为什么要 +1"的钥匙。
四、补码:现代计算机唯一在用的
规则:反码 + 1。
+5 → 0000 0101
-5 → 1111 1011
现在:
0000 0101
+ 1111 1011
= 1 0000 0000
8 位装不下,最高位进位溢出被丢弃 → 0000 0000 = 0。加减法统一了。
下面是我实际跑出来的验证:
========== 1. 补码的形式:+5 与 -5 ==========
+5 补码 = 0000 0101 (最高位 0,正数)
-5 补码 = 1111 1011 (最高位 1,负数)
-5 当无符号数读 = 251 ,恰好是 256 - 5 = 251
补码相加:5 + (-5) = 0 ← 进位溢出被丢弃,结果为 0
注意第三行:把 -5 的补码 1111 1011 当成无符号数读,正好是 251 = 256 - 5。这不是巧合,下面时钟那节会解释为什么。
五、用「时钟」理解为什么是"取反加 1"
这是最不容易忘的记法。
假设时钟只有 8 个刻度(0~7)。现在指针在 3,要拨到 1,有两种拨法:
- 倒拨 2 格:3 − 2 = 1
- 正拨 6 格:3 + 6 = 9 → 越过 8 回卷 → 1
所以在这个「模 8」的世界里,−2 和 +6 是同一件事。而 6 恰好就是 2 的"补数"(8 − 2 = 6)。
二进制同理。8 位世界的模数是 2⁸ = 256:
-5 ≡ 256 - 5 = 251 = 1111 1011 ← 这就是 -5 的补码
而"取反加 1"只是快速求 256 − x 的位运算技巧:
~x + 1 = 256 - x
你不需要背它,推导一遍就懂了。
六、范围公式:为什么 byte 是 -128~127,而不是对称的 ±127
这是考试最爱考的点。补码把 1000 0000 这个"多出来的"码位分配给了 -128。
实测各类型的取值范围:
========== 9. 各类定长整数的取值范围 ==========
byte ( 8 位) -128 ~ 127
short (16 位) -32768 ~ 32767
int (32 位) -2147483648 ~ 2147483647
long (64 位) -9223372036854775808 ~ 9223372036854775807
规律:最小值 = -2^(n-1),最大值 = 2^(n-1) - 1,负数比正数多一个
| 类型 | 位数 | 取值范围 |
|---|---|---|
| byte | 8 | -128 ~ 127 |
| short | 16 | -32768 ~ 32767 |
| int | 32 | -2³¹ ~ 2³¹-1 |
| long | 64 | -2⁶³ ~ 2⁶³-1 |
规律:最小值 = -2^(n-1),最大值 = 2^(n-1) - 1。负数比正数多一个,因为 0 占用了正数那边的位置。
记忆技巧:1000 0000 按"取反加 1"倒推,取反得 0111 1111 = 127,加 1 = 128,符号位为 1 → -128。而 −128 再取相反数会溢出(结果是它自己),这就是它被叫做"奇怪的数"的原因。
七、Java 里能用到的几个实战点
下面这段代码全部实测过,输出附在后面:
public class BitOpsTest {
/** 求相反数:等价于 -x */
static int negate(int x) { return ~x + 1; }
/** 位运算取绝对值 */
static int absByBit(int x) {
int mask = x >> 31; // 正数得 0,负数得 -1(全 1)
return (x + mask) ^ mask;
}
static boolean isOdd(int x) { return (x & 1) == 1; }
/** 打印一个 byte 的 8 位补码形式 */
static String bits8(int v) {
StringBuilder sb = new StringBuilder();
for (int i = 7; i >= 0; i--) {
sb.append((v >> i) & 1);
if (i == 4) sb.append(' ');
}
return sb.toString();
}
}
7.1 求相反数:~x + 1
5 ~x+1 = -5 -x = -5 一致吗:是
-5 ~x+1 = 5 -x = 5 一致吗:是
0 ~x+1 = 0 -x = 0 一致吗:是
100 ~x+1 = -100 -x = -100 一致吗:是
2147483647 ~x+1 = -2147483647 -x = -2147483647 一致吗:是
-2147483648 ~x+1 = -2147483648 -x = -2147483648 一致吗:是
最后一行的 -2147483648(也就是 Integer.MIN_VALUE)值得单独说:~x+1 和 -x 结果相同,都是它自己。因为 2³¹ 超出了 int 的正数范围,正数那边根本没有对应的数,两种写法都绕回了 MIN_VALUE。所以这行显示"一致",但两个结果在数学上都是错的——MIN_VALUE 的相反数在 int 里无法表示。
7.2 异或交换两个数,不用临时变量
交换前:a = 12,b = 25
交换后:a = 25,b = 12
原理:a ^ b 保留了"两者不同的那些位",连续异或三次就能把值换过来。自带补习性质(自己异或自己是 0),不需要额外空间。代价是可读性差,实际工程里更推荐直接写临时变量。
7.3 判奇偶:(x & 1) == 1
7 (x & 1) == 1 → true 与 x % 2 != 0 一致吗:true
8 (x & 1) == 1 → false 与 x % 2 != 0 一致吗:true
-9 (x & 1) == 1 → true 与 x % 2 != 0 一致吗:true
0 (x & 1) == 1 → false 与 x % 2 != 0 一致吗:true
-9 那一行是重点:在 Java 里 -9 % 2 得到的是 -1 而不是 1,所以判断奇数必须写 x % 2 != 0,不能写 x % 2 == 1。而 x & 1 不受符号影响,永远是 0 或 1,更省心也更快。
7.4 取绝对值(位运算版)
7 absByBit = 7 Math.abs = 7 一致吗:是
-7 absByBit = 7 Math.abs = 7 一致吗:是
0 absByBit = 0 Math.abs = 0 一致吗:是
-2147483648 absByBit = -2147483648 Math.abs = -2147483648 一致吗:是
mask = x >> 31 是算术右移:正数得 0,负数得全 1(即 -1)。
(x + mask) ^ mask 对正数不变,对负数则完成"取反加 1"。
MIN_VALUE 那一行又一次暴露边界:Math.abs(Integer.MIN_VALUE) 也是负数。这不是实现 bug,而是"绝对值超出了 int 正数范围"的必然结果——用 long 接收才能得到正确的 2147483648。
7.5 >> 与 >>>
-8 >> 1 = -4 算术右移,符号位补 1,负数还是负数
-8 >>> 1 = 2147483644 逻辑右移,高位补 0,当成无符号数看
Integer.toBinaryString(-8) = 11111111111111111111111111111000
Integer.toBinaryString(-8 >> 1) = 11111111111111111111111111111100
>> 是算术右移:补符号位,负数的性质保持不变,相当于"除以 2 向下取整"。
>>> 是逻辑右移:高位一律补 0,把二进制串当成无符号数看待,负数会瞬间变成很大的正数。
>>> 是 Java 独有的设计(C 语言没有这个运算符),因为 Java 没有无符号整数类型,需要它来做位操作和哈希散列。
7.6 溢出:最大值加 1 绕回最小值
Integer.MAX_VALUE = 2147483647
Integer.MAX_VALUE + 1 = -2147483648
Integer.MIN_VALUE = -2147483648
Integer.MIN_VALUE - 1 = 2147483647
这就是补码"环形"最直观的体现:世界是圆的,走到头就从另一头出来。
八、最容易混的四个概念
| 概念 | 一句话定义 | 例子(8 位) |
|---|---|---|
| 原码 | 符号位 + 绝对值 | -5 = 1000 0101 |
| 反码 | 负数数值位取反 | -5 = 1111 1010 |
| 补码 | 反码 + 1,机器实际存储的形式 | -5 = 1111 1011 |
| 移码 | 补码符号位取反,用于浮点阶码 | -5 = 0111 1011 |
移码顺带说一下:它存在的意义是让浮点数的阶码能用"无符号比较"来比大小。IEEE 754 里 float 的 8 位阶码用的就是移码(偏移量 127)。
九、做题时的通用套路
给负数求补码:写成二进制 → 符号位填 1 → 数值位取反 → 加 1。
由补码求真值:先看最高位,是 1 就说明是负数,对它"取反加 1"得到正值 x,答案就是 −x。
实测验证一下:
========== 8. 由 16 位补码求真值:1111 1111 1111 0000 ==========
0xFFF0 当作 16 位补码读出来 = -16
取反加 1 得到的正值 = 16,所以真值 = -16
1111 1111 1111 0000 → 取反得 0000 0000 0000 1111 → 加 1 = 16 → 真值 −16。与套路完全一致。
十、小结
- 原码直观但有两个毛病:0 有双份、加减法不通用。
- 反码是过渡品,它证明了"回卷后数值正确",补码在此基础上 +1 解决了 ±0。
- 补码让减法统一成加法,最高位进位自然丢弃,这就是"只有加法器"的实现基础。
- 范围公式:最小值 −2^(n-1),最大值 2^(n-1) − 1,负数比正数多一个。
~x + 1就是 256 − x,即"取反加 1"是求补数的位运算技巧,不必死记。- 所有位运算实战都要留意
Integer.MIN_VALUE:取相反数、取绝对值在它身上都会"溢出成自己",这是唯一一个没有正数对应的值。 - 判断奇数用
x % 2 != 0而不是x % 2 == 1(负数取模结果是 -1),或者干脆用(x & 1) == 1。
本文所有运行输出均来自
Program/BitOpsTest.java,可用
javac -encoding UTF-8 BitOpsTest.java && java -Dfile.encoding=UTF-8 BitOpsTest复现。

浙公网安备 33010602011771号