☰
补码符号位能参与运算的真正原因:模运算、负权重与位宽边界
2026/10/2 11:08:52 网站建设 项目流程

1. 从"时钟拨回3小时"说起:补码到底在描述什么

"补码的符号位为什么能参与运算"这个问题,我在带新人的时候被问过不下十次。绝大多数人背的是口诀:正数原样、负数取反加一、符号位照抄、加法直接加。但一旦被追问"最高位明明是符号,凭什么能和数值位一起送进加法器,算完结果还对",回答就退化成"书上就是这么写的"。这篇文章就把这层窗户纸捅破:补码是什么、符号位为什么压根不是一位特殊位、减法是怎么在硬件里消失的、以及为什么符号位参与了运算,溢出却依然存在。

你只需要懂二进制和竖式加法,剩下的都能看懂。如果连二进制都不太熟,也没关系,我会把每一步展开。整篇的路线是:先用钟表建立模运算的直觉,再回头看原码和反码为什么做不到,最后落到"最高位权重为负"这个真正的原因上,顺便把 1024QAM 那种"位宽到底该给几位"的账也算清楚。

1.1 一个每天都在用、却没人点破的模运算

先别看电路,看钟表。

现在是 10 点,你要把时间往回拨 3 个小时,你会怎么拨?直接倒着拨到 7 点。但你也可以往前拨 9 个小时,时针一样停在 7 点。因为在"12 小时制"这个系统里,-3 和 +9 的效果完全相同——它们对 12 同余。

写成式子:-3 ≡ 12 - 3 = 9 (mod 12)。

这就是模运算。模运算有一个极其好用的性质:减法可以整体换掉。减去一个数,等价于加上"这个数在模意义下的补数"。12 减 3 得 9,9 就是 3 在模 12 下的补数,"补码"这个名字的来源就在这里。

计算机的加法器本质上也是一只钟。一个 8 位加法器,表盘上有 2^8 = 256 个刻度,从 00000000 数到 11111111,再加 1 就回到 00000000——第 9 位那个进位被物理地丢掉了,因为它没有地方存。这不是设计缺陷,恰恰是整个体系赖以成立的基石。数字电路的寄存器只有 8 个触发器,第 9 位进位没有落脚的触发器,它自然就溢出了。补码的聪明之处,就是把这个"无奈的丢失"变成了免费的减法。

1.2 为什么"丢掉最高位进位"不是丢数据

很多人第一次看到 3 + (-5) 的竖式会发慌:最高位明明产生了进位,直接扔掉,数据不就丢了吗?

答案是:在固定位宽的系统里,那个进位本来就不属于系统内部。在 256 刻度的表盘上,250 加 10 等于 260,指针停在 4,因为 260 ≡ 4 (mod 256)。最高位那一个 1 代表的是"整整转了一圈"(256),它对表盘上的读数没有任何贡献。把它扔掉,反而让结果落回到圈内。

换个说法:加法器回答的问题是"转完之后指针停在哪",而不是"一共转了几圈"。圈数被丢弃是任务的一部分,不是失误。理解了这一点,后面的一切都顺理成章——包括为什么减法可以变成加法,包括为什么符号位能加进去。

我常常用一个更方便的类比:把 8 位寄存器想象成一块只有 8 个格子的里程表,走到 99999999 再加 1 就翻回 00000000。司机从来不关心"翻过几次表",只关心当前读到的格子里的数。硬件也是这样。

1.3 把钟表的规则翻译成二进制

约定一个 8 位系统,模 m = 2^8 = 256,规则只有两条:

  • 非负数 x(且 x < 128)的补码表示就是它自己;
  • 负数 -x 的补码表示是 256 - x。

拿 -5 举例:256 - 5 = 251,写成二进制是 11111011。验算一下:251 + 5 = 256 ≡ 0 (mod 256),说明 251 确实是 5 的加法逆元,也就是"负 5"。再看 -1:256 - 1 = 255 = 11111111,所以全 1 就是 -1,这也解释了一个常见现象——把一个数按位取反再加 1 就能得到它的相反数,因为 255 - x + 1 = 256 - x。

请注意一个容易被忽略的细节:凡是最高位为 1 的补码数,它的无符号值都落在 128~255,映射到有符号解释就是 -128~-1。符号位不是谁规定出来的,是数完一圈之后自然浮现的结果。8 位补码的范围是 -128~127,负数比正数多一个,因为唯一的那个 0 站在非负那一边,把 256 个刻度切成了 128 和 128 两半,但非负半区要从 0 数起,所以正数只能到 127。

到这里,我们只用了"模"这一个概念,还没有碰符号位。下一节我们看看,如果不用补码,会遇到什么麻烦。

2. 原码和反码的窘境:它们为什么必须把符号位单独拎出来

教材上通常把原码、反码、补码并列介绍,然后直接跳到结论。但如果不讲清楚原码和反码到底卡在哪里,"符号位参与运算"这件事就失去了对比的参照物。这一节我们算三笔账,看看前两种编码在硬件上到底付出了什么代价。

2.1 原码:符号归符号,数值归数值

原码是最符合人类直觉的表示法:最高位当符号位(0 正 1 负),其余位放绝对值。

  • +5 = 00000101
  • -5 = 10000101
  • +3 = 00000011
  • -3 = 10000011

看着很舒服。但你要用它做 -5 + 3,ALU 就没法直接算了。它得先做一串判断:

  1. 取出两个符号位,比较是否相同;
  2. 不相同,说明要做减法,于是比较两个绝对值的大小(5 > 3);
  3. 用大的减小的:5 - 3 = 2;
  4. 结果符号取绝对值大的那个数的符号,也就是负;
  5. 最后把符号位和数值位拼起来:10000010,即 -2。

一个加法指令,在硬件里被拆成"符号比较 + 绝对值比较 + 减法 + 符号回填"四五个步骤,关键路径长得可怕。更麻烦的是 0:00000000 和 10000000 都表示 0,硬件必须专门判断"结果是不是 0",如果是,还得强制把符号位清零,否则会出现"正零加负零得到负零"这种荒唐事。

原码之所以还在用,是因为它有别的优点——比如浮点数的尾数符号就是单独处理的,取绝对值、比较大小的运算用原码很省事。但做加减法,它是真的不行。

2.2 反码:零有两个,进位还得绕回去

反码的思路是:正数不变,负数是"对应的正数按位取反"。

  • +5 = 00000101
  • -5 = 11111010(对 00000101 取反)
  • +3 = 00000011
  • -3 = 11111100

反码比原码进了一步,因为符号位可以参与相加了。拿 -5 + 3 试试:11111010 + 00000011 = 11111101,反码解释回来:符号位是 1,取反得 00000010,也就是 -2。对了。

但反码有个致命的尾巴——循环进位。再算一个 -5 + 6:11111010 + 00000110 = 1 00000000,最高位迸出个进位,这时候不能像补码那样直接扔掉,必须把这个进位加回到最低位去:00000000 + 1 = 00000001,结果是 1。这个操作叫 end-around carry,硬件上得从最高位拉一根线回到最低位的进位输入,多一级加法延迟,还容易在级联时出问题。

更头疼的还是零:反码里 00000000(正零)和 11111111(负零)都表示零。判断相等、判断是否为零,都要多写一套逻辑。

2.3 三张账单放在一起看

把三种编码在 8 位下的关键指标列出来,差别一目了然:

对比项原码反码补码
表示范围-127 ~ 127-127 ~ 127-128 ~ 127
零的个数2 个(正零/负零)2 个(正零/负零)1 个
符号位能否直接参与加法不能能能
是否需要循环进位不需要(但要做符号判断)需要不需要
减法能否复用加法器不能基本能,但有额外电路完全能
硬件代价高(多级判断)中(回环进位 + 双零判断)低

补码用两个变化换来了硬件上的极大简化:一是把零统一成一个,二是把 -128 这个不对称的边界"白送"进来。代价是理解起来不那么直观——但只要你接受了 1.1 节的钟表模型,这一切都很自然。

3. 真正的原因:最高位的权重是负的

前面我们用"模运算"解释了补码为什么能算,但那个解释还隔着一层。真正一句话说清"符号位为什么能参与运算"的,是权重视角。这一节是全篇的核心。

3.1 给每一位重新标一次价

我们先回忆一下无符号数的位权。一个 8 位无符号数 b7 b6 b5 b4 b3 b2 b1 b0,它的值是:

b7×128 + b6×64 + b5×32 + b4×16 + b3×8 + b2×4 + b1×2 + b0×1

每一位都有自己的"价格",从右往左依次翻倍。

现在,如果我说有符号补码的位权表,只是把最高位那一项的价格改成 -128,其他七位完全不变,你会不会觉得有点突然?但这就是事实:

值 =-b7×128+ b6×64 + b5×32 + b4×16 + b3×8 + b2×4 + b1×2 + b0×1

验证一下 -5 的补码 11111011:

-128×1 + 64×1 + 32×1 + 16×1 + 8×1 + 0×2 + 2×1 + 1×1 = -128 + 64 + 32 + 16 + 8 + 0 + 2 + 1 = -128 + 123 = -5

完全正确。再验证 +3 的补码 00000011:最高位是 0,那一项贡献 0,剩下 2 + 1 = 3。也对。

3.2 符号位从来不是"特殊位"

这才是"符号位能参与运算"的真正答案:它压根不是一位被特别标记的位,它只是一位普通的位置给了一个负的权重而已。加法器做的事情,从头到尾就是"每一位乘以它的权重再求和",它根本没有"符号位"这个概念。你把它当符号,它是一位权重为负的数值位;你不把它当符号,它就是最高位。两种说法描述的是同一条位串,只是解释方式不同。

这里有一个非常反直觉但极其重要的推论:同一块加法器硬件,对同样的位串,既可以做有符号加法,也可以做无符号加法,因为加法动作完全一样。区别只在于结果怎么被解释、溢出标志怎么被设置、以及比较指令和右移指令采用哪套规则。

这也解释了为什么计算机里没有"有符号加法器"和"无符号加法器"两套电路。我在做 FPGA 定点运算的时候就吃过这个甜头:一个流水线加法器核,只要改一下溢出判定和上层的数据类型声明,就能同时服务两种解释,逻辑资源一点没多花。

3.3 手算四个例子,把这件事钉死

光说原理容易浮在空中,我们老老实实做四道竖式,全部用 8 位。

例 1:3 + (-5)

00000011 (+3) + 11111011 (-5) ----------- 11111110 (无第9位进位)

按权重解释 11111110:最高位是 1,贡献 -128,其余 64+32+16+8+4+2 = 126,合起来 -128 + 126 = -2。也可以按"无符号值减 256"算:254 - 256 = -2。答案 -2,正确。

例 2:5 + (-3)

00000101 (+5) + 11111101 (-3) ----------- 1 00000010 (第9位进位丢弃)

丢掉进位后的 00000010 就是 2,正确。注意这里最高位进位确实发生了,被丢弃是正常操作。

例 3:-1 + (-2)

11111111 (-1) + 11111110 (-2) ----------- 1 11111101 (丢弃进位)

11111101 按权重算:-128 + 64 + 32 + 16 + 8 + 4 + 0 + 1 = -128 + 125 = -3。正确。

例 4:-128 + (-1)

10000000 (-128) + 11111111 (-1) ----------- 1 01111111 (丢弃进位)

结果是 01111111,也就是 +127。错。-128 加 -1 应该等于 -129,但 8 位补码装不下 -129。这就是溢出。注意:符号位老老实实参与了运算,运算过程本身没有毛病,问题出在"结果的数值超出了表盘能表示的范围"。

例 4 非常重要,它划清了界限:符号位能参与运算,靠的是权重设计;但权重设计只能保证"结果落在范围内时是对的",不能凭空变出范围之外的表示能力。

3.4 顺手把补码还原成原码

既然讲到了权重,就顺便说清楚"已知补码求原码"这件事,这是热词里问得最多的。

方法一:再求一次补。对负数补码,按位取反再加 1,得到的位串就是它绝对值的原码(正数补码不变)。比如 11111011 取反得 00000100,加 1 得 00000101,即 5,加上负号就是 -5。

方法二:找最右边的第一个 1。从右往左扫描,找到第一个 1,这个 1 以及它右边的所有位保持不动,左边的所有位(包括最高位)全部取反。拿 11111011 试:最右边的第一个 1 在 bit0,它左边是 1111101,取反得 0000010,拼上那个 1 就是 00000101,等于 5。再拿 11111100(-4)试:最右第一个 1 在 bit2,左边 11111 取反得 00000,拼上 bit2 的 1 和右边两个 0,得到 00000100 = 4,符号为负,-4,正确。

方法二其实是方法一的化简版本,因为"取反加一"里加的那个 1 会让最右侧连续的 1 全部进位变 0,最后恰好等价于"保留右侧第一个 1 不动"。心算的时候方法二快得多,我在课堂上讲解题时基本都用它。

3.5 减法是怎么在硬件里消失的

既然符号位是普通位,那么 a - b 就可以写成一件事:a 加上 b 的相反数。而 b 的相反数在补码里就是"按位取反再加 1":

a - b = a + (~b) + 1

注意那个 +1 可以塞进最低位的进位输入里,不需要额外的一级加法。于是硬件上一条减法路径就变成了:把 b 的每一位接一个反相器,反相器的输出接到加法器的 B 端,同时把加法器的最低位进位输入拉高。控制端一个信号切过去,加法器原地变成减法器,额外成本几乎为零。

这一点在面试里经常被当作考点:一位减法器需要多少个门?答案是几乎不需要,一个反相器加一条进位线就够了。这就是补码体系最漂亮的地方——它不是让减法更快,而是让减法不存在。

4. 符号位能加进运算,不代表结果永远正确

我见过太多人被"符号位参与运算"这句话带偏,以为既然符号位都参与运算了,那结果自然是可信的。不是的。这一节专门讲边界:什么时候会错,怎么判断,以及错在哪里。

4.1 溢出的本质:表盘不够大

回到 3.3 节的例 4:-128 + (-1)。两个负数相加,结果却是正数 127。这不是"符号位失效",而是"结果超出了表示范围"。

用模运算的说法最清楚:-128 在模 256 下是 128,-1 是 255,128 + 255 = 383,383 mod 256 = 127。模运算本身一点没错,只是 127 这个余数已经跑到正半区去了,它对应不到 -129 这个真实答案上。数学是对的,表示是不够的。

所以判断溢出的关键,不是看结果对不对,而是看"真实和是否落在 [-128, 127] 内"。

4.2 三种判定方法,从硬件到软件

方法一:进位异或法。设 C_out 是最高位向外的进位,C_7 是次高位向最高位的进位,则

溢出 = C_out XOR C_7

用 3.3 节的例 4 验证:10000000 + 11111111 时,bit7 相加是 1+1+0(进位输入为 0),产生 C_out = 1,而 C_7 = 0,异或得 1,判定溢出。再看 5 + (-3) 那例:C_out = 1,C_7 = 1,异或得 0,无溢出。再看 127 + 1:C_out = 0,C_7 = 1,异或得 1,判定溢出。三例全对。

这个方法的妙处在于,它只用两个进位信号就能给出溢出标志,硬件上就是半个异或门。所以在处理器里,溢出的典型处理就是"保留低 n 位结果,把越界的那部分信息放进一个状态标志里,让上层用指令去检测"。

方法二:同号判定法。两个操作数符号相同,而结果符号与它们不同,就是溢出。这个规则适合用软件或者手工检查,人眼一眼就能看出来。上面例 4 就是典型的"负负得正",一眼假。

方法三:双符号位(变形补码)。把符号位扩成两位,00 表示正、11 表示负。加法之后,如果两位符号变成了 01,是正溢出;变成 10,是负溢出;仍然是 00 或 11,则正常。这个方案的好处是硬件判决更直接,代价是多一位寄存器。一些教学机和早期处理器用过这种办法。

4.3 符号扩展:为什么高位补的是符号位

还有一个必然要面对的细节:8 位数要放进 16 位寄存器,高位补什么?

答案是补符号位。正数补 0,负数补 1。-5 的 8 位是 11111011,扩到 16 位就是 11111111 11111011。

为什么这样补?还是用权重算。设最高位是 b(0 或 1),扩展 k 位后,新增的权重和是:

b×(2^(n-1) + 2^n + ... + 2^(m-2)) = b×(2^(m-1) - 2^(n-1))

而新权重的最高项是 -b×2^(m-1),这项和上面的正项一合并,就只剩 -b×2^(n-1)。也就是说,新增的那些位互相抵消了,整个数的值一点没变。这个推导值得你自己在纸上写一遍,写完之后"符号扩展"就再也不是一个需要记忆的规则,而是一个必然结果。

反过来说,无符号数扩展时补的是 0,因为那些新增位的权重全是正的,补 1 会真的加上一大块数值。这也是为什么在 C 语言里,有符号和无符号混合运算时,如果发生扩展,行为会很微妙——类型提升规则决定了到底补 0 还是补 1。

4.4 位宽截断:更常见的隐形错误

溢出是"装不下更大的数",截断是"丢掉了低位"。这两个在工程里都很常见,但后者更阴险。

比如一个 16 位中间结果要写回 8 位寄存器,你只取了低 8 位。如果这个数的真实值在 [-128, 127] 内,没问题;一旦超出,低位保留、高位丢弃,结果可能变得面目全非。8 位下 200 + 100 = 300,取低 8 位是 300 mod 256 = 44,而如果按有符号解释,44 还是 44,看着像个正常数,静默出错。

我在做音频定点处理时就栽过这个跟头:一个中间增益系数用 32 位算,写回 16 位时没做饱和判断,结果在大音量段出现了刺耳的"翻转噪声"——因为波形在正负两端被硬生生折断了。正确做法是饱和截断:超出上限就钳到最大值,低于下限就钳到最小值。硬件上饱和逻辑只比回绕多几个比较器,代价很小,但听感上的差别是灾难级的。

5. 落到代码和硬件上的几个真实坑

理论讲完了,下面是我这些年实际踩过、或者看着别人踩过的坑。每一条都跟补码和符号位相关,不看原理是想不到的。

5.1 C/C++ 里 int 与 unsigned 混用的静默陷阱

这段代码我见过无数次:

int a = -1; unsigned int b = 1; if (a < b) { printf("a < b\n"); }

很多人以为会打印。实际上不会。因为a在与b比较时被转换成unsigned int,-1 的位模式 0xFFFFFFFF 解释成无符号就是 4294967295,远大于 1。这个转换没有警告,编译器默默做了。

同样的坑还有:

unsigned int i = 5; if (i - 10 > 0) { // 名字看着像在防负数,其实永远为真 // ... }

i - 10在无符号下回绕成 4294967291,判断结果为真。正确写法是if (i > 10)。

根本原因就是 3.2 节那句话:位模式相同,解释不同,而 C 的类型规则决定了什么时候用哪种解释。判断依据不是数学,是类型的转换顺序。我现在的习惯是:凡是可能相减的两个无符号量,要么先转成有符号再比较,要么用显式的"减法前判大小"写法,绝不依赖a - b > 0这种表达。

5.2 算术右移与逻辑右移

右移一位有两种语义:

  • 逻辑右移:高位一律补 0,适合无符号数。
  • 算术右移:高位补符号位,等价于除以 2 并向下取整,适合有符号数。

把 -8(11111000)算术右移一位,得到 11111100,也就是 -4,正确。如果误用逻辑右移,得到 01111100 = 124,完全变味。

在 C 语言里,对有符号负数右移的行为是"实现定义"的,绝大多数编译器和平台选择算术右移,但它不是标准强制的。所以我在需要严格可移植的定点代码里,从不直接对有符号负数做>>,要么先转无符号再手动处理符号,要么用乘法或除法明确表达意图。汇编层面就清晰得多,x86 有 SAR(算术)和 SHR(逻辑)两条指令,写汇编时不会有歧义,写 C 的时候反而容易糊。

5.3 浮点数的符号位是另一套逻辑

必须澄清一个常见误解:IEEE 754 浮点数的最高位确实叫符号位,但它的语义跟补码完全不是一回事。浮点数用的是"符号 + 阶码 + 尾数"的表示,最高位是一个独立的符号标记,不参与数值的权重计算。

所以当你听到"符号位能参与运算"这句话时,它只适用于整数的补码表示。浮点的加减法要经历对阶、尾数相加、规格化、舍入一长串步骤,符号的处理是这套流程里独立的一环。把两者的直觉混在一起,会得出很多错误的结论,比如"浮点的 -0 和 +0 也是同一个零"——它们在比较时相等,但在位模式上完全不同,1/0 和 1/-0 也给出不同的结果。

5.4 定点 DSP 与 Q 格式:位宽该给几位

做定点算法时,我们通常约定 Qm.n 格式:1 位符号位,m 位整数部分,n 位小数部分,总位宽 m + n + 1。比如 Q1.14 是 16 位,范围是 -2 到 1.99993896...

对同一个物理量,用什么样的 Q 格式,本质上就是在"范围"和"精度"之间做交换:小数位越多,精度越高但动态范围越小;整数位越多,动态范围越大但量化噪声越明显。这个权衡没有标准答案,要看你关注的信号幅度分布。

我自己的经验是:先把信号的理论动态范围算清楚,留 3~6 dB 的余量,剩下的位全给精度。中途如果想改格式,一定要检查所有截断层,因为一次疏忽的回绕就足以毁掉整条链路的信噪比。

6. 有位宽的账要算:从 1024QAM 的 10bit 说起

热词里冒出来一个"1024QAM 的符号位长为啥是 10bit",我猜很多人被"符号位长"这个说法绕住了。这节把位宽的账算一遍,你会发现它跟前面讲的补码表示范围是同一件事。

6.1 位宽与取值个数的关系

先记住两个基本事实:

  • n 位二进制串一共有 2^n 种不同的组合。
  • 如果按无符号解释,这 2^n 种组合对应 0 到 2^n - 1。
  • 如果按补码解释,这 2^n 种组合对应 -2^(n-1) 到 2^(n-1) - 1。

关键在于:位宽决定的是"有多少种不同的值",而不是"值有多大"。无论有没有符号位,n 位的表达能力都是 2^n 个不同取值,一个不多一个不少。有符号解释只是把这 2^n 个取值重新分配到了负数那一侧。

位宽无符号范围有符号范围取值个数
4 位0 ~ 15-8 ~ 716
8 位0 ~ 255-128 ~ 127256
10 位0 ~ 1023-512 ~ 5111024
16 位0 ~ 65535-32768 ~ 3276765536

6.2 1024 个点为什么正好要 10 位

1024QAM 的"1024"指的是星座图上共有 1024 个点,一个符号携带 log2(1024) = 10 个比特的信息。

如果你的代码或测试向量把这 1024 个点编号,用一个有符号整数来承载这个编号,那么 10 位是一个下界:2^10 = 1024,恰好覆盖 1024 个不同取值,范围是 -512 到 511。用 9 位只有 512 种组合,装不下;用 11 位则浪费一位。

所以"10bit"这个数字的真正含义是:在有符号解释下表示 1024 个不同取值所需的最小位宽是 10。它说的是"承载这个符号的整数位宽是 10 位(按有符号解释)",而不是"符号占据了 10 个比特"。这两句话差得远。

如果你看到有人把它理解成"符号位占了 10 位",那基本就是把"有符号表示的位宽"和"符号位本身"混为一谈了。符号位永远只有一位,剩下的是数值位。

6.3 真实调制器里其实是分开算的

还有一层实际实现的细节:在真正的调制链路里,I 和 Q 两路通常是分开处理的。1024 个点排成 32×32 的方阵,每一路只需要 32 个电平。而要表示 32 个不同取值,5 位就够了(2^5 = 32),有符号解释下范围是 -16 到 15。

也就是说,同一个"10bit",在"整体编号"的视角和"I/Q 分开"的视角下,指向的是两种完全不同的数据结构。前者的 10 位是一个整体的索引或软值,后者的 5×2 位是两个独立的坐标。写代码的时候如果混了,就会出现"明明只给了 5 位,却要放下 1024 个点"这种令人抓狂的 bug。

我个人的做法是:在算法的不同层,明确标注每一层用的是"联合索引"还是"分量坐标",并在函数签名上写清楚位宽和符号性。定点链路里最怕的就是这种隐式的语义漂移,它不会引起编译错误,只会让最终的星座图悄悄歪掉。

6.4 饱和还是回绕,这是个工程决策

再补一句关于位宽的实操心法。当数值超出当前位宽时,有两种处理:

  • 回绕(wrap):丢掉高位,保留低位,也就是纯粹的补码截断。硬件最省,一个截位就完事。代价是信号会出现突变的尖峰。
  • 饱和(saturate):钳到最大或最小值。多几个比较器,但异常值不会变成反向的极值。

在通信和音频这类对波形连续性敏感的场合,基本一律选饱和。因为回绕会把一个正的大值变成负的大值,在星座图上表现为跳到了对面象限,接收端解调出来就是一串连续错误。而在某些纯算术场景(比如哈希、校验和),回绕反而是期望行为,因为它保住了模运算的数学性质。

选哪个,取决于你把这段位宽看成"一个圈"还是"一个区间"。看成圈,就回绕;看成区间,就饱和。这个判断比记住任何一条规则都重要。

最后说说我自己的体会。刚学补码那会儿,我花了很长时间在"符号位怎么能参与运算"上打转,原因是我一直把"符号位"当成一个有特殊身份的东西,总想找一个"它凭什么被允许混进去"的理由。后来是手写了一个 8 位加法器的 Verilog 才想通:加法器根本不知道什么是符号位,它只是把两个向量按权重加起来,权重表里最高位是负数而已。意识到这一点之后,原码反码补码、符号扩展、溢出判定、有符号无符号的转换,全都变成了同一件事的不同侧面。如果你也卡在同一个地方,建议你拿纸把 -128 到 127 的补码表从头到尾写一遍,再随便挑十对数字做竖式加法,亲手丢掉几次最高位进位,那种"原来如此"的感觉会来得比看任何解释都快。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询