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