补码是什么?为什么byte的范围是-128~127,负数要比整数多1个?

261 阅读2分钟

引言

在学习任何一门编程语言时,都会首先学习基本数据类型。其中n位的整数类型(有符号)的范围是:

2n12n11-2^{n-1} \sim 2^{n-1}-1

例如C++中的int8_tchar,以及Java中的byte,都是8位有符号整数,范围是128127-128 \sim 127, 有没有想过为什么负数会比正数多一个呢?

另外,如果byte num = 127, 什么num + 1=-128呢?

要想搞清楚背后的原理,就要知道二进制补码表示法

二进制补码表示法

二进制补码是计算机中表示有符号整数的标准方式(例如Java 中 byte、short、int、long 等整数类型均采用补码存储)。在补码出现之前,计算机曾用原码反码表示有符号整数

原码

  • 表示方式:最高位为符号位(0 = 正、1 = 负),数值位为二进制本身。例如 8 位原码+3=00000011-3=10000011
  • 存在问题
    • 存在 “正负 0”(+0=00000000-0=10000000),浪费一个存储位
    • 减法需单独处理(3-2 不能直接用 3+(-2) 的原码相加,结果会出错)。
  3  -> 00000011
+ -2 -> 10000010
————————————————
        10000101  -> -5, 结果错误

反码

  • 表示方式:正数反码 = 原码;负数反码 = 符号位不变,数值位按位取反。例如 8 位反码:+3=00000011-3=11111100
  • 存在问题
    • 减法问题:与原码相比,可以处理简单的减法,但是需要进位循环增加硬件按复杂度。更致命的是,无法处理“正负 0” 的歧义导致运算逻辑矛盾
     3 -> 00000011
  + -2 -> 11111101
  ————————————————
         100000000  -> 超出8位,需要进位循环,
         舍弃最高位 `1`,将进位加回最低位 → `00000000 + 1 = 00000001`,结果为1,正确;
         
     3 -> 00000011
  + -3 -> 11111100
  ————————————————
          11111111  -> -0, 但是3-3结果应该是0,存在歧义

补码

补码的核心改进正是针对反码的缺陷:

  • 消除 “正负 0”:仅保留 00000000 作为 0,10000000 分配给 -128(8 位),无歧义;
  • 无需进位循环:补码加法的最高位进位直接舍弃(模运算特性),硬件实现简单;

关键概念:模

“模” 是指存储位数对应的 “溢出值”,即该位数能表示的最大无符号数 + 1。例如8位整数(如 byte)模 = 2⁸ = 256(能表示 0~255 共 256 个值);

补码计算步骤

  1. 正数的补码,与原码完全一致
规则:符号位为 0,数值位为该数的二进制表示
示例(以 8 位为例):
+5 的原码 = 00000101 → 补码 = 00000101;
+127 的原码 = 01111111 → 补码 = 01111111
  1. 负数的补码:两种等价计算方式
    • 方式 1:补码 = 模 - 绝对值的原码
    • 方式 2:补码 = 负数的反码 + 1
示例:计算 8 位 -3 的补码
先求绝对值 3 的原码:00000011
方式 1:模(256) - 3 = 253 → 253 的二进制 = 11111101(即 -3 的补码);
方式 2:-3 的原码 = 10000011 → 反码(符号位不变,数值位取反)= 11111100 → 反码 + 1 = 11111101(与方式 1 结果一致)。

这里解释一下为什么两个方式等价,也就是: 绝对值的原码=负数的反码+1模 - 绝对值的原码 = 负数的反码 + 1,假设A>0A>0为正数,则A<0-A<0为负数

绝对值的原码=负数的反码+1  256A的原码=A的反码+1     255=A的反码+A的原码  \begin{align*} 模 - 绝对值的原码 &= 负数的反码 + 1 \\     256-A的原码 &= -A的反码 + 1 \\          255 &= -A的反码 + A的原码     \end{align*}

根据负数反码的定义符号位不变,数值位按位取反,-A的反码和A的原码中,其中一方为1的位另一方就为0,相加刚好是11111111 = 255

如何根据补码求数值?

根据 补码=绝对值的原码补码 = 模 - 绝对值的原码得:

绝对值的原码=补码绝对值的原码 = 模 - 补码

得到绝对值的原码,计算出绝对值,然后再根据补码得符号位判断正负,例如:

补码10000001表示什么数?
首先用模-补码:100000000 - 10000001 = 01111111 = 127
由于补码第一位是1,所以10000001表示-127

反过来验证一下:
补码 = 负数的反码 + 1
10000001 = 10000000 + 1

补码 = 模 - 绝对值的原码
10000001 = 100000000 - 01111111

为什么负数范围比正数范围多1?

关键在于0得表示,在二进制补码表示法中,0只有唯一的表示方式:00000000,那么10000000在补码中表示什么?

绝对值的原码 = 100000000 - 10000000 = 10000000 = 128,符号位为1,所以是-128

也就是说原码中的表示-0的10000000在补码中表示-128。由于舍弃了-0,并分配给了最小的负数-128,所以负数的范围就比正数多1个

为什么 127 + 1 = -128?

知道了补码的原理后就很好理解了,127 + 1 = 01111111 + 1 = 10000000 = -128