引言
在学习任何一门编程语言时,都会首先学习基本数据类型。其中n位的整数类型(有符号)的范围是:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> − 2 n − 1 ∼ 2 n − 1 − 1 -2^{n-1} \sim 2^{n-1}-1 </math>−2n−1∼2n−1−1
例如C++中的int8_t和char,以及Java中的byte,都是8位有符号整数,范围是 <math xmlns="http://www.w3.org/1998/Math/MathML"> − 128 ∼ 127 -128 \sim 127 </math>−128∼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)的原码相加,结果会出错)。
- 存在 "正负 0"(
plaintext
3 -> 00000011
+ -2 -> 10000010
------------------------------------------------
10000101 -> -5, 结果错误
反码
- 表示方式 :正数反码 = 原码;负数反码 = 符号位不变,数值位按位取反。例如 8 位反码:
+3=00000011,-3=11111100。 - 存在问题 :
- 减法问题:与原码相比,可以处理简单的减法,但是需要进位循环增加硬件按复杂度。更致命的是,无法处理"正负 0" 的歧义导致运算逻辑矛盾
plaintext
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 个值);
补码计算步骤
- 正数的补码,与原码完全一致
plaintext
规则:符号位为 0,数值位为该数的二进制表示
示例(以 8 位为例):
+5 的原码 = 00000101 → 补码 = 00000101;
+127 的原码 = 01111111 → 补码 = 01111111
- 负数的补码:两种等价计算方式
- 方式 1:补码 = 模 - 绝对值的原码
- 方式 2:补码 = 负数的反码 + 1
plaintext
示例:计算 8 位 -3 的补码
先求绝对值 3 的原码:00000011
方式 1:模(256) - 3 = 253 → 253 的二进制 = 11111101(即 -3 的补码);
方式 2:-3 的原码 = 10000011 → 反码(符号位不变,数值位取反)= 11111100 → 反码 + 1 = 11111101(与方式 1 结果一致)。
这里解释一下为什么两个方式等价,也就是: <math xmlns="http://www.w3.org/1998/Math/MathML"> 模 − 绝对值的原码 = 负数的反码 + 1 模 - 绝对值的原码 = 负数的反码 + 1 </math>模−绝对值的原码=负数的反码+1,假设 <math xmlns="http://www.w3.org/1998/Math/MathML"> A > 0 A>0 </math>A>0为正数,则 <math xmlns="http://www.w3.org/1998/Math/MathML"> − A < 0 -A<0 </math>−A<0为负数
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> 模 − 绝对值的原码 = 负数的反码 + 1 256 − A 的原码 = − A 的反码 + 1 255 = − A 的反码 + A 的原码 \begin{align*} 模 - 绝对值的原码 &= 负数的反码 + 1 \\ 256-A的原码 &= -A的反码 + 1 \\ 255 &= -A的反码 + A的原码 \end{align*} </math>模−绝对值的原码 256−A的原码 255=负数的反码+1=−A的反码+1=−A的反码+A的原码
根据负数反码的定义符号位不变,数值位按位取反 ,-A的反码和A的原码中,其中一方为1的位另一方就为0,相加刚好是11111111 = 255
如何根据补码求数值?
根据 <math xmlns="http://www.w3.org/1998/Math/MathML"> 补码 = 模 − 绝对值的原码 补码 = 模 - 绝对值的原码 </math>补码=模−绝对值的原码得:
<math xmlns="http://www.w3.org/1998/Math/MathML" display="block"> 绝对值的原码 = 模 − 补码 绝对值的原码 = 模 - 补码 </math>绝对值的原码=模−补码
得到绝对值的原码,计算出绝对值,然后再根据补码得符号位判断正负,例如:
plaintext
补码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