This commit is contained in:
krahets
2026-04-14 18:06:19 +08:00
parent 17b2a0b630
commit cf0747ba3e
131 changed files with 604 additions and 609 deletions

View File

@@ -4363,11 +4363,11 @@
</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>Прежде всего нужно отметить, что <strong>числа хранятся в компьютере в виде «дополнительного кода»</strong>. Прежде чем разбирать причины такого решения, сначала дадим определения всем трем способам представления.</p>
<ul>
<li><strong>Прямой код</strong>: старший бит двоичного представления числа рассматривается как знаковый, где <span class="arithmatex">\(0\)</span> означает положительное число, а <span class="arithmatex">\(1\)</span> - отрицательное; остальные биты представляют значение числа.</li>
<li><strong>Обратный код</strong>: для положительного числа обратный код совпадает с прямым; для отрицательного числа он получается инверсией всех битов прямого кода, кроме знакового бита.</li>
<li><strong>Дополнительный код</strong>: для положительного числа дополнительный код совпадает с прямым; для отрицательного числа он получается добавлением <span class="arithmatex">\(1\)</span> к его обратному коду.</li>
<li><strong>Прямой код</strong>: старший бит двоичного представления числа рассматривается как знаковый, где <span class="arithmatex">\(0\)</span> означает положительное число, а <span class="arithmatex">\(1\)</span> - отрицательное. Остальные биты представляют значение числа.</li>
<li><strong>Обратный код</strong>: для положительного числа обратный код совпадает с прямым. Для отрицательного числа он получается инверсией всех битов прямого кода, кроме знакового бита.</li>
<li><strong>Дополнительный код</strong>: для положительного числа дополнительный код совпадает с прямым. Для отрицательного числа он получается добавлением <span class="arithmatex">\(1\)</span> к его обратному коду.</li>
</ul>
<p>На рисунке 3-4 показаны способы преобразования между прямым, обратным и дополнительным кодами.</p>
<p><img alt="Преобразования между прямым, обратным и дополнительным кодами" class="animation-figure" src="../number_encoding.assets/1s_2s_complement.png" /></p>
@@ -4377,8 +4377,8 @@
<div class="arithmatex">\[
\begin{aligned}
&amp; 1 + (-2) \newline
&amp; \rightarrow 0000 \; 0001 + 1000 \; 0010 \newline
&amp; = 1000 \; 0011 \newline
&amp; \rightarrow 0000 \. 0001 + 1000 \. 0010 \newline
&amp; = 1000 \. 0011 \newline
&amp; \rightarrow -3
\end{aligned}
\]</div>
@@ -4386,45 +4386,45 @@
<div class="arithmatex">\[
\begin{aligned}
&amp; 1 + (-2) \newline
&amp; \rightarrow 0000 \; 0001 \; \text{(прямой код)} + 1000 \; 0010 \; \text{(прямой код)} \newline
&amp; = 0000 \; 0001 \; \text{(обратный код)} + 1111 \; 1101 \; \text{(обратный код)} \newline
&amp; = 1111 \; 1110 \; \text{(обратный код)} \newline
&amp; = 1000 \; 0001 \; \text{(прямой код)} \newline
&amp; \rightarrow 0000 \. 0001 \. \text{(прямой код)} + 1000 \. 0010 \. \text{(прямой код)} \newline
&amp; = 0000 \. 0001 \. \text{(обратный код)} + 1111 \. 1101 \. \text{(обратный код)} \newline
&amp; = 1111 \. 1110 \. \text{(обратный код)} \newline
&amp; = 1000 \. 0001 \. \text{(прямой код)} \newline
&amp; \rightarrow -1
\end{aligned}
\]</div>
<p>С другой стороны, **в прямом коде у нуля есть два представления: <span class="arithmatex">\(+0\)</span> и <span class="arithmatex">\(-0\)</span> **. Это означает, что числу ноль соответствуют два разных двоичных кода, что может приводить к неоднозначности. Например, если в условном выражении не различать положительный и отрицательный ноль, можно получить ошибочный результат. А если специально обрабатывать такую неоднозначность, придется вводить дополнительные проверки, что может снизить вычислительную эффективность компьютера.</p>
<div class="arithmatex">\[
\begin{aligned}
+0 &amp; \rightarrow 0000 \; 0000 \newline
-0 &amp; \rightarrow 1000 \; 0000
+0 &amp; \rightarrow 0000 \. 0000 \newline
-0 &amp; \rightarrow 1000 \. 0000
\end{aligned}
\]</div>
<p>Как и прямой код, обратный код тоже страдает от неоднозначности положительного и отрицательного нуля, поэтому компьютеры ввели <u>дополнительный код (2's complement)</u>. Сначала посмотрим на процесс преобразования отрицательного нуля из прямого кода в обратный, а затем в дополнительный:</p>
<div class="arithmatex">\[
\begin{aligned}
-0 \rightarrow \; &amp; 1000 \; 0000 \; \text{(прямой код)} \newline
= \; &amp; 1111 \; 1111 \; \text{(обратный код)} \newline
= 1 \; &amp; 0000 \; 0000 \; \text{(дополнительный код)} \newline
-0 \rightarrow \. &amp; 1000 \. 0000 \. \text{(прямой код)} \newline
= \. &amp; 1111 \. 1111 \. \text{(обратный код)} \newline
= 1 \. &amp; 0000 \. 0000 \. \text{(дополнительный код)} \newline
\end{aligned}
\]</div>
<p>При добавлении <span class="arithmatex">\(1\)</span> к обратному коду отрицательного нуля возникает перенос, но длина типа <code>byte</code> составляет всего 8 бит, поэтому переполнившаяся в 9-й бит единица отбрасывается. Иными словами, <strong>дополнительный код отрицательного нуля равен <span class="arithmatex">\(0000 \; 0000\)</span> и совпадает с дополнительным кодом положительного нуля</strong>. Значит, в представлении дополнительного кода существует только один ноль, и проблема неоднозначности положительного и отрицательного нуля тем самым устраняется.</p>
<p>При добавлении <span class="arithmatex">\(1\)</span> к обратному коду отрицательного нуля возникает перенос, но длина типа <code>byte</code> составляет всего 8 бит, поэтому переполнившаяся в 9-й бит единица отбрасывается. Иными словами, <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>Однако <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>
<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}
&amp; (-127) + (-1) \newline
&amp; \rightarrow 1111 \; 1111 \; \text{(прямой код)} + 1000 \; 0001 \; \text{(прямой код)} \newline
&amp; = 1000 \; 0000 \; \text{(обратный код)} + 1111 \; 1110 \; \text{(обратный код)} \newline
&amp; = 1000 \; 0001 \; \text{(дополнительный код)} + 1111 \; 1111 \; \text{(дополнительный код)} \newline
&amp; = 1000 \; 0000 \; \text{(дополнительный код)} \newline
&amp; \rightarrow 1111 \. 1111 \. \text{(прямой код)} + 1000 \. 0001 \. \text{(прямой код)} \newline
&amp; = 1000 \. 0000 \. \text{(обратный код)} + 1111 \. 1110 \. \text{(обратный код)} \newline
&amp; = 1000 \. 0001 \. \text{(дополнительный код)} + 1111 \. 1111 \. \text{(дополнительный код)} \newline
&amp; = 1000 \. 0000 \. \text{(дополнительный код)} \newline
&amp; \rightarrow -128
\end{aligned}
\]</div>
<p>Ты, вероятно, уже заметил, что все приведенные выше вычисления были операциями сложения. Это указывает на важный факт: <strong>аппаратные схемы внутри компьютера в основном проектируются на основе операций сложения</strong>. Причина в том, что сложение по сравнению с другими операциями (например умножением, делением и вычитанием) проще реализуется на аппаратном уровне, легче распараллеливается и выполняется быстрее.</p>
<p>Обрати внимание: это не означает, что компьютер умеет только складывать. <strong>Комбинируя сложение с некоторыми базовыми логическими операциями, компьютер может реализовать и другие математические операции</strong>. Например, вычитание <span class="arithmatex">\(a - b\)</span> можно преобразовать в сложение <span class="arithmatex">\(a + (-b)\)</span> ; умножение и деление можно свести к многократному сложению или вычитанию.</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 байта , но почему диапазон значений у <code>float</code> намного больше, чем у <code>int</code> ? Это выглядит парадоксально, ведь <code>float</code> должен еще представлять дробные числа, а значит диапазон вроде бы должен быть меньше.</p>
<p>На самом деле <strong>это связано с тем, что число с плавающей точкой <code>float</code> использует другой способ представления</strong>. Обозначим двоичное число длиной 32 бита как:</p>
@@ -4455,12 +4455,12 @@ b_{31} b_{30} b_{29} \ldots b_2 b_1 b_0
<p><img alt="Пример вычисления float по стандарту IEEE 754" class="animation-figure" src="../number_encoding.assets/ieee_754_float.png" /></p>
<p align="center"> Рисунок 3-5 &nbsp; Пример вычисления float по стандарту IEEE 754 </p>
<p>Посмотрим на рисунок 3-5: если взять пример <span class="arithmatex">\(\mathrm{S} = 0\)</span> , <span class="arithmatex">\(\mathrm{E} = 124\)</span> , <span class="arithmatex">\(\mathrm{N} = 2^{-2} + 2^{-3} = 0.375\)</span> , то получим:</p>
<p>Как видно на рисунке 3-5, если взять пример <span class="arithmatex">\(\mathrm{S} = 0\)</span> , <span class="arithmatex">\(\mathrm{E} = 124\)</span> , <span class="arithmatex">\(\mathrm{N} = 2^{-2} + 2^{-3} = 0.375\)</span> , то получим:</p>
<div class="arithmatex">\[
\text { val } = (-1)^0 \times 2^{124 - 127} \times (1 + 0.375) = 0.171875
\]</div>
<p>Теперь мы можем ответить на исходный вопрос: <strong>в представлении <code>float</code> присутствуют биты экспоненты, поэтому его диапазон значений намного больше, чем у <code>int</code></strong>. Согласно приведенным выше вычислениям, максимально возможное положительное число для <code>float</code> равно <span class="arithmatex">\(2^{254 - 127} \times (2 - 2^{-23}) \approx 3.4 \times 10^{38}\)</span> ; если изменить бит знака, получим минимальное отрицательное число.</p>
<p><strong>Хотя число с плавающей точкой <code>float</code> расширяет диапазон значений, побочным эффектом становится потеря точности</strong>. Целочисленный тип <code>int</code> использует все 32 бита для представления числа, и числа распределены равномерно; а из-за существования битов экспоненты у <code>float</code> чем больше число, тем больше обычно становится разница между двумя соседними представимыми значениями.</p>
<p>Теперь мы можем ответить на исходный вопрос: <strong>в представлении <code>float</code> присутствуют биты экспоненты, поэтому его диапазон значений намного больше, чем у <code>int</code></strong>. Согласно приведенным выше вычислениям, максимально возможное положительное число для <code>float</code> равно <span class="arithmatex">\(2^{254 - 127} \times (2 - 2^{-23}) \approx 3.4 \times 10^{38}\)</span>. Если изменить бит знака, получим минимальное отрицательное число.</p>
<p><strong>Хотя число с плавающей точкой <code>float</code> расширяет диапазон значений, побочным эффектом становится потеря точности</strong>. Целочисленный тип <code>int</code> использует все 32 бита для представления числа, и числа распределены равномерно. А из-за существования битов экспоненты у <code>float</code> чем больше число, тем больше обычно становится разница между двумя соседними представимыми значениями.</p>
<p>Как показано в таблице 3-2, значения экспоненты <span class="arithmatex">\(\mathrm{E} = 0\)</span> и <span class="arithmatex">\(\mathrm{E} = 255\)</span> имеют специальный смысл и <strong>используются для представления нуля, бесконечности, <span class="arithmatex">\(\mathrm{NaN}\)</span> и т.д.</strong></p>
<p align="center"> Таблица 3-2 &nbsp; Значение поля экспоненты </p>