反码与补码

反码与补码

课程:Java 语言程序设计
教师:王建民
班级:信2505-2 学号:20253909 姓名:刘家瑞


一、一句话本质

计算机里只有加法器,没有减法器。所有减法都变成「加一个负数」。

反码、补码这一整套东西,存在的唯一目的就是:让减法能用加法实现,并且 +0 和 -0 不打架。

下面用 8 位(byte)举例。


二、原码:最直观,但有两个毛病

原码 = 最高位当符号位(0 正 1 负),剩下 7 位存绝对值。

+5 → 0000 0101
-5 → 1000 0101

毛病很明显:

  1. 0 有两种写法。0000 0000 和 1000 0000 都表示 0,机器判断"是不是 0"要判两次。
  2. 加减法不通用。+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。与套路完全一致。


十、小结

  1. 原码直观但有两个毛病:0 有双份、加减法不通用。
  2. 反码是过渡品,它证明了"回卷后数值正确",补码在此基础上 +1 解决了 ±0。
  3. 补码让减法统一成加法,最高位进位自然丢弃,这就是"只有加法器"的实现基础。
  4. 范围公式:最小值 −2^(n-1),最大值 2^(n-1) − 1,负数比正数多一个。
  5. ~x + 1 就是 256 − x,即"取反加 1"是求补数的位运算技巧,不必死记。
  6. 所有位运算实战都要留意 Integer.MIN_VALUE:取相反数、取绝对值在它身上都会"溢出成自己",这是唯一一个没有正数对应的值。
  7. 判断奇数用 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 复现。

posted @ 2026-09-25 14:05  liujerry  阅读(2)  评论(0)    收藏  举报