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