在阅读《C和指针》时,书中提到“在不同机器上,char 类型可能默认是 signed 或 unsigned”。这引发了我的好奇:变量有无符号到底有什么区别?常听说的“8位无符号”又是什么?带着这些疑问,我展开了一系列思考,最终一路挖到了计算机底层的补码原理。
一、 数据的本质:灯泡与二进制
计算机底层没有数字,只有电。电只有“通电”和“断电”两种状态,我们用 1 和 0 表示。
- 一个灯泡 = 一个“位(Bit)”。
- 8个灯泡排成一排 = 一个“字节(Byte)”。
这8个灯泡从右往左,有着不同的“权重”(2的幂):
128, 64, 32, 16, 8, 4, 2, 1(即 2727 到 2020)。 如果8个灯泡全亮(11111111),加起来就是 128+64+…+1 = 255。这就是8位无符号数的最大值。(这不就是信息课的十进制二进制互换吗)
二、 有符号与无符号:正负号牌
既然8个灯泡全亮是255,那负数怎么表示?首先必须有办法区分正负,我们才能想办法保存他们
于是,计算机规定:最左边的那颗灯泡(最高位)拿来做“正负号牌”。
- 牌子灭(
0):代表正数。 - 牌子亮(
1):代表负数。 这就是C语言里“有符号(signed)”与“无符号(unsigned)”的根本区别: - 无符号数:8个灯泡全用来算数值,范围是
0 ~ 255。 - 有符号数:最左边是号牌,剩下7位算数值,范围是
-128 ~ 127。(因为一半的数值空间被分配给了负数)。 比如灯泡11111101(最左边是1,代表负数),在补码规则下,它表示 -3。
三、 补码的终极奥义:里程表与溢出
知道了怎么区分正负,那么负数的具体数值该怎么保存呢?这就是著名的补码(Two’s Complement)。
1. 破车的里程表
假设一辆破车的里程表只有 3位数字(000 到 999)。如果现在显示 003,你想让它变成 000,你有两个选择:
- 往后倒 3 公里(做减法:3 - 3 = 0)。
- 往前开 997 公里(做加法:3 + 997 = 1000,最高位溢出扔掉,表盘变成
000)。
在只有3位数的世界里,“-3”和“+997”的效果一模一样!
2. 计算机的补码
计算机的CPU里只有加法器,没有减法器。为了简化电路,科学家决定:**以后遇到减法,一律变成加法!**怎么找这个“魔法替身数字”?
- 3位里程表的溢出阈值是 1000(破车到999就归零了)。
- 我们要算
3 + x = 1000,所以x = 997。
回到8个灯泡(二进制):
- 8个灯泡的溢出阈值是 256(也就是二进制的11111111,二的八次方加七次方加…你可以自己算算试试)。一旦加到256,第9个灯泡亮起,前8个灯泡全部清零。
- 我们要算
3 + x = 256,所以x = 253。
这就是253的由来! 它不是为了好玩捏造的,而是为了让3加上它之后刚好能溢出归零,被迫算出来的一个数字。
四、 深度解密:“取反加一”是怎么来的?
我们要找的是 256−3=253。但计算机不会算 256−3,怎么办?科学家用了一个极其聪明的“凑整”技巧(以-3为例):
第一步:按位取反(用全1去减)
8个灯泡全亮是 11111111(也就是255)。
用255减一个数非常简单,不需要借位,直接“按位取反”就行。
3的灯泡是 00000011,取反后变成 11111100(也就是252)。
(就相当于在十进制里面255-3)
第二步:末尾加一(补上缺失的1)
我们算出了252,但目标是253,差了1。
为什么会少了1?因为255比256刚好少了1!
(就像你在破车的里程表上,全亮是999(而不是1000,因为1000就已经溢出了,你没办法减了)。你要找的是 1000 - 3 = 997,但你先用 999 - 3,得到了 996。因为999比1000少了1,所以你算出的996也比真正的目标997少了1。为了补上这个缺口,你必须在末尾加1,才能凑成997。)
所以,我们需要把刚才少算的1补回来,也就是在末尾加1(加上 2020)。
11111100 + 1 = 11111101(也就是253)。
大功告成!把 3 和 253 加起来:00000011 + 11111101 = 100000000(第9位溢出扔掉,剩下 00000000,也就是0)。3 + (-3) 完美等于 0。
⚠️ 细节避坑:末尾加0也是 2020,为什么不行? 这里要分清“权重”和“数值”。2020 是权重(砝码)。最后一位填0,就是
0 × 2^0 = 0;填1,才是1 × 2^0 = 1。因为我们距离目标差了1,所以必须加1,加0等于什么都没干。
五、 为什么要用补码
读到现在,你可能有个疑问:为什么用补码?我们不能规定这样,最左侧用0/1标志他是不是一个负数,然后直接在剩下的七位(或更多)里面存储绝对值吗?难道是科学家想炫耀非凡的数学天赋?(补码确实很是巧妙,特别是你自己算一下更有体会) 答案是:你说的这种方案,早期的计算机真的试过!但后来被无情淘汰了,因为它会让计算机的电路变得极其复杂(也就是极其贵)。 我们用8个灯泡来对比一下“你的方案(原码)”和“现在的方案(补码)”,你就瞬间明白为什么计算机要选补码了。
1. 直接存绝对值 + 号牌
假设我们用这种方法表示数字:
- 最左边是号牌(0正,1负)。
- 剩下7位存这个数的绝对值。
我们来写几个数:
+5的灯泡:00000101-3的灯泡:10000011(最左边1是负,后面0000011是3)
灾难一:加法完全失效
现在我们要算 5 + (-3)。计算机只会做加法,它直接把这俩灯泡加起来:
00000101 (+5)+ 10000011 (-3)----------- 10001000 (这是 -8!)结果是 -8,完全错了!为什么?因为计算机把号牌当成了数值(128)去加了。 如果你要用原码算对,CPU必须:
- 先检查两个数的号牌。
- 发现是正数加负数,实际上应该是“大数减小数”。
- 比较 5 和 3 谁的绝对值大。
- 用 5 - 3 = 2。
- 再把号牌定成正数。 你看,为了做加法,计算机还得造一个减法器,还得造一个比较大小的电路! 这违背了“让电路最简化”的初衷。
灾难二:有两个零
00000000是 +0。10000000是 -0。 计算机每次判断“这个数是不是0”,还得判断两次。极其反人类。
2. 现在的方案(补码):让减法变成加法
科学家一拍大腿:我们不要“绝对值+号牌”了!我们把负数变成“里程表的补数”,让它们直接相加就能出正确结果!
回到刚才的例子:
+5的灯泡:00000101-3的灯泡:11111101(这就是补码)
现在直接相加:
00000101 (+5)+ 11111101 (-3,其实就是253)----------- 100000010 (第9位溢出扔掉,剩下 00000010,也就是 2)完美!不需要判断符号,不需要比较大小,不需要减法器,只要一个加法器,电路极其简单、便宜!
而且,补码只有唯一的零(00000000),再也没有 -0 这种奇葩。
这就是有关有符号、无符号与补码的一切,感谢您的阅读,这可能也是这个博客第一篇技术相关文章。
分享文章
生成精美分享图或复制链接,与更多人分享本文。