This commit is contained in:
krahets
2023-12-02 06:24:11 +08:00
parent 5783c402bf
commit d20d8b3ee1
107 changed files with 1685 additions and 1745 deletions

View File

@ -3384,11 +3384,11 @@
<h1 id="33">3.3 &nbsp; 数字编码 *<a class="headerlink" href="#33" title="Permanent link">&para;</a></h1>
<div class="admonition note">
<p class="admonition-title">Note</p>
<p>在本书中,标题带有 * 符号的是选读章节。如果你时间有限或感到理解困难,可以先跳过,等学完必读章节后再单独攻克。</p>
<p>在本书中,标题带有 * 符号的是选读章节。如果你时间有限或感到理解困难,可以先跳过,等学完必读章节后再单独攻克。</p>
</div>
<h2 id="331">3.3.1 &nbsp; 整数编码<a class="headerlink" href="#331" title="Permanent link">&para;</a></h2>
<p>在上一节的表格中我们发现,所有整数类型能够表示的负数都比正数多一个,例如 <code>byte</code> 的取值范围是 <span class="arithmatex">\([-128, 127]\)</span> 。这个现象比较反直觉,它的内在原因涉及原码、反码、补码的相关知识。</p>
<p>首先需要指出,<strong>数字是以“补码”的形式存储在计算机中的</strong>。在分析这样做的原因之前,我们首先给出三者的定义。</p>
<p>在上一节的表格中我们发现,所有整数类型能够表示的负数都比正数多一个,例如 <code>byte</code> 的取值范围是 <span class="arithmatex">\([-128, 127]\)</span> 。这个现象比较反直觉,它的内在原因涉及原码、反码、补码的相关知识。</p>
<p>首先需要指出,<strong>数字是以“补码”的形式存储在计算机中的</strong>。在分析这样做的原因之前,首先给出三者的定义。</p>
<ul>
<li><strong>原码</strong>:我们将数字的二进制表示的最高位视为符号位,其中 <span class="arithmatex">\(0\)</span> 表示正数,<span class="arithmatex">\(1\)</span> 表示负数,其余位表示数字的值。</li>
<li><strong>反码</strong>:正数的反码与其原码相同,负数的反码是对其原码除符号位外的所有位取反。</li>
@ -3407,7 +3407,7 @@
&amp; \rightarrow -3
\end{aligned}
\]</div>
<p>为了解决此问题,计算机引入了「反码 1's complement」。如果我们先将原码转换为反码并在反码下计算 <span class="arithmatex">\(1 + (-2)\)</span> ,最后将结果从反码转回原码,则可得到正确结果 <span class="arithmatex">\(-1\)</span></p>
<p>为了解决此问题,计算机引入了「反码 1's complement」。如果我们先将原码转换为反码并在反码下计算 <span class="arithmatex">\(1 + (-2)\)</span> ,最后将结果从反码转回原码,则可得到正确结果 <span class="arithmatex">\(-1\)</span></p>
<div class="arithmatex">\[
\begin{aligned}
&amp; 1 + (-2) \newline
@ -3418,7 +3418,7 @@
&amp; \rightarrow -1
\end{aligned}
\]</div>
<p>另一方面,<strong>数字零的原码有 <span class="arithmatex">\(+0\)</span><span class="arithmatex">\(-0\)</span> 两种表示方式</strong>。这意味着数字零对应两个不同的二进制编码,可能会带来歧义。比如在条件判断中,如果没有区分正零和负零,则可能会导致判断结果出错。而如果我们想处理正零和负零歧义,则需要引入额外的判断操作,可能会降低计算机的运算效率。</p>
<p>另一方面,<strong>数字零的原码有 <span class="arithmatex">\(+0\)</span><span class="arithmatex">\(-0\)</span> 两种表示方式</strong>。这意味着数字零对应两个不同的二进制编码,可能会带来歧义。比如在条件判断中,如果没有区分正零和负零,则可能会导致判断结果出错。而如果我们想处理正零和负零歧义,则需要引入额外的判断操作,可能会降低计算机的运算效率。</p>
<div class="arithmatex">\[
\begin{aligned}
+0 &amp; \rightarrow 0000 \; 0000 \newline
@ -3434,7 +3434,7 @@
\end{aligned}
\]</div>
<p>在负零的反码基础上加 <span class="arithmatex">\(1\)</span> 会产生进位,但 <code>byte</code> 类型的长度只有 8 位,因此溢出到第 9 位的 <span class="arithmatex">\(1\)</span> 会被舍弃。也就是说,<strong>负零的补码为 <span class="arithmatex">\(0000 \; 0000\)</span> ,与正零的补码相同</strong>。这意味着在补码表示中只存在一个零,正负零歧义从而得到解决。</p>
<p>还剩最后一个疑惑:<code>byte</code> 类型的取值范围是 <span class="arithmatex">\([-128, 127]\)</span> ,多出来的一个负数 <span class="arithmatex">\(-128\)</span> 是如何得到的呢?我们注意到,区间 <span class="arithmatex">\([-127, +127]\)</span> 内的所有整数都有对应的原码、反码和补码,并且原码和补码之间可以互相转换</p>
<p>还剩最后一个疑惑:<code>byte</code> 类型的取值范围是 <span class="arithmatex">\([-128, 127]\)</span> ,多出来的一个负数 <span class="arithmatex">\(-128\)</span> 是如何得到的呢?我们注意到,区间 <span class="arithmatex">\([-127, +127]\)</span> 内的所有整数都有对应的原码、反码和补码,并且原码和补码之间可以互相转换。</p>
<p>然而,<strong>补码 <span class="arithmatex">\(1000 \; 0000\)</span> 是一个例外,它并没有对应的原码</strong>。根据转换方法,我们得到该补码的原码为 <span class="arithmatex">\(0000 \; 0000\)</span> 。这显然是矛盾的,因为该原码表示数字 <span class="arithmatex">\(0\)</span> ,它的补码应该是自身。计算机规定这个特殊的补码 <span class="arithmatex">\(1000 \; 0000\)</span> 代表 <span class="arithmatex">\(-128\)</span> 。实际上,<span class="arithmatex">\((-1) + (-127)\)</span> 在补码下的计算结果就是 <span class="arithmatex">\(-128\)</span></p>
<div class="arithmatex">\[
\begin{aligned}
@ -3446,10 +3446,10 @@
&amp; \rightarrow -128
\end{aligned}
\]</div>
<p>你可能已经发现,上述所有计算都是加法运算。这暗示着一个重要事实:<strong>计算机内部的硬件电路主要是基于加法运算设计的</strong>。这是因为加法运算相对于其他运算(比如乘法、除法和减法)来说,硬件实现起来更简单,更容易进行并行化处理,运算速度更快。</p>
<p>你可能已经发现,上述所有计算都是加法运算。这暗示着一个重要事实:<strong>计算机内部的硬件电路主要是基于加法运算设计的</strong>。这是因为加法运算相对于其他运算(比如乘法、除法和减法)来说,硬件实现起来更简单,更容易进行并行化处理,运算速度更快。</p>
<p>请注意,这并不意味着计算机只能做加法。<strong>通过将加法与一些基本逻辑运算结合,计算机能够实现各种其他的数学运算</strong>。例如,计算减法 <span class="arithmatex">\(a - b\)</span> 可以转换为计算加法 <span class="arithmatex">\(a + (-b)\)</span> ;计算乘法和除法可以转换为计算多次加法或减法。</p>
<p>现在我们可以总结出计算机使用补码的原因:基于补码表示,计算机可以用同样的电路和操作来处理正数和负数的加法,不需要设计特殊的硬件电路来处理减法,并且无须特别处理正负零的歧义问题。这大大简化了硬件设计,提高了运算效率。</p>
<p>补码的设计非常精妙,因篇幅关系我们就先介绍到这里,建议有兴趣的读者进一步深了解。</p>
<p>补码的设计非常精妙,因篇幅关系我们就先介绍到这里,建议有兴趣的读者进一步深了解。</p>
<h2 id="332">3.3.2 &nbsp; 浮点数编码<a class="headerlink" href="#332" title="Permanent link">&para;</a></h2>
<p>细心的你可能会发现:<code>int</code><code>float</code> 长度相同,都是 4 bytes ,但为什么 <code>float</code> 的取值范围远大于 <code>int</code> ?这非常反直觉,因为按理说 <code>float</code> 需要表示小数,取值范围应该变小才对。</p>
<p>实际上,<strong>这是因为浮点数 <code>float</code> 采用了不同的表示方式</strong>。记一个 32-bit 长度的二进制数为:</p>
@ -3462,15 +3462,15 @@ b_{31} b_{30} b_{29} \ldots b_2 b_1 b_0
<li>指数位 <span class="arithmatex">\(\mathrm{E}\)</span> :占 8 bits ,对应 <span class="arithmatex">\(b_{30} b_{29} \ldots b_{23}\)</span></li>
<li>分数位 <span class="arithmatex">\(\mathrm{N}\)</span> :占 23 bits ,对应 <span class="arithmatex">\(b_{22} b_{21} \ldots b_0\)</span></li>
</ul>
<p>二进制数 <code>float</code> 对应值的计算方法:</p>
<p>二进制数 <code>float</code> 对应值的计算方法</p>
<div class="arithmatex">\[
\text {val} = (-1)^{b_{31}} \times 2^{\left(b_{30} b_{29} \ldots b_{23}\right)_2-127} \times\left(1 . b_{22} b_{21} \ldots b_0\right)_2
\]</div>
<p>转化到十进制下的计算公式:</p>
<p>转化到十进制下的计算公式</p>
<div class="arithmatex">\[
\text {val}=(-1)^{\mathrm{S}} \times 2^{\mathrm{E} -127} \times (1 + \mathrm{N})
\]</div>
<p>其中各项的取值范围:</p>
<p>其中各项的取值范围</p>
<div class="arithmatex">\[
\begin{aligned}
\mathrm{S} \in &amp; \{ 0, 1\}, \quad \mathrm{E} \in \{ 1, 2, \dots, 254 \} \newline
@ -3522,7 +3522,7 @@ b_{31} b_{30} b_{29} \ldots b_2 b_1 b_0
</table>
</div>
<p>值得说明的是,次正规数显著提升了浮点数的精度。最小正正规数为 <span class="arithmatex">\(2^{-126}\)</span> ,最小正次正规数为 <span class="arithmatex">\(2^{-126} \times 2^{-23}\)</span></p>
<p>双精度 <code>double</code> 也采用类似 <code>float</code> 的表示方法,在此不做赘述。</p>
<p>双精度 <code>double</code> 也采用类似 <code>float</code> 的表示方法,在此不做赘述。</p>
<!-- Source file information -->