Компьютеры - это удивительные машины, которые позволяют нам выражать врожденные человеческие концепции и идеи. Мы можем смотреть видео, отправлять сообщения и даже вычислять сложные арифметические операции. Однако эти высокоуровневые концепции - просто абстракции машинного кода. Удивительно, но все данные на машинном уровне хранятся в виде двоичных чисел, 0 или 1. В этой статье мы коснемся верхушки айсберга и обсудим, как положительные и отрицательные числа хранятся в машине.

В программировании переменные - это небольшие строительные блоки, из которых состоят более крупные программы. Тип переменной определяет, какие значения она может содержать и какие операции с ней можно выполнять. В языке программирования C переменные статически типизированы, и тип должен быть указан при объявлении переменной.

Напротив, динамически типизированные языки, такие как Python и Javascript, не требуют указания типов переменных. Статически типизированные языки предлагают преимущество проверки типов во время компиляции, которая выявляет небольшие ошибки типа на ранней стадии. В то время как языки с динамической типизацией имеют тенденцию быть более лаконичными по своему синтаксису, проверка типов происходит во время времени выполнения, что может затруднить обнаружение тривиальных ошибок типа в процессе разработки. .

Следующая строка кода объявляет и определяет целое число с именем x в C.

int x = 11;

Когда компилятор достигает этой строки кода, для хранения этого целочисленного значения выделяется память. Размер целого числа зависит от используемого компьютера и компилятора, но обычно размер целого числа составляет 4 байта как на 32-битных, так и на 64-битных машинах, использующих gcc. Если вам нужна дополнительная информация об аппаратном обеспечении вашего компьютера, введите команду uname -m в командной строке терминала. Но поскольку размер зависит от платформы и реализации, единственный надежный способ узнать размер целого числа на вашем персональном компьютере - использовать оператор sizeof().

printf("The size of an integer on my machine is %d\n", sizeof(int));
>> The size of an integer on my machine is 4

Стандарт C99 гарантирует, что минимально возможные целые числа, которые могут быть сохранены, находятся в диапазоне от -2,147,483,648 до 2,147,483,647 для целых чисел со знаком и от 0 до 4,294,967,295 для целых чисел без знака. . По умолчанию целые числа подписаны, что позволяет нам представлять отрицательные числа. Бывают ситуации, когда использование квалификатора unsigned имеет больше смысла. Например, когда мы хотим определить размер переменной, мы можем использовать sizeof() оператор, который возвращает целое число без знака типа size_t, потому что размер чего-либо может быть только положительным.

Что касается ограничений, как мы определили диапазон возможных целых чисел, которые могут быть представлены целочисленной переменной в C? Ранее я упоминал, что стандарт C99 гарантирует как минимум точное представление целых чисел от -2,147,483,648 до 2,147,483,647. Чтобы понять почему, мы должны понять, как компьютеры представляют числа в памяти.

На самом базовом уровне компьютеры работают в двоичном формате. Двоичная - это система счисления с основанием два, и она представляет данные в терминах 0 и 1. бит - это наименьшая единица данных, содержащая единственное двоичное значение 0 или 1. Байты - это более крупная единица данных, а 1 байт содержит 8 бит. Целые числа - это 4 байта, которые содержат 32 бита для представления данных.

Наша современная культура привыкла к десятичной системе счисления. У нас, людей, 10 пальцев, поэтому десятичная система счисления приходит нам интуитивно. Если вы не знакомы с системой обозначений с основанием два, вот небольшое руководство о том, как читать двоичные файлы.

Скажем, мы хотим представить 123 в десятичной системе, мы бы просто написали 123. Каждая единица (единицы, десятки, сотни) разрядов представлена ​​основанием (10) в n-й степени, начиная с 0. Вот почему в десятичной системе счисления. , место единиц - это место единиц, потому что 10 в 0-й степени равно 1. Разряд десятков означает 10 в 1-й степени, 100-е место - 10 во 2-й степени, и так далее.

Мы можем использовать ту же формулу (основание 2 в n-й степени) для представления числа 123 в базе 2. С 8 битами 123 будет представлено как таковое:

Двоичное представление числа 123 выделено красным, но для наглядности вот оно снова:

0111 1011 /* this is 123 in binary */

С помощью формулы BASE ^ BIT-WIDTH мы можем определить максимальное и минимальное значения целых чисел со знаком. Для целых чисел со знаком крайний левый бит, часто называемый старшим значащим битом, зарезервирован для обозначений (положительных или отрицательных). Следуя этой логике, 1 бит используется для указателей, а 31 бит остается для хранения числовых данных. Это позволяет использовать максимальное число 2³¹ минус 1 (потому что мы должны представить 0) - 2 147 483 647 и минимум - 2 147 483 648.

Когда целое число без знака, для хранения числовых данных доступен самый старший бит. Таким образом, беззнаковое 4-байтовое int имеет минимальное значение 0 и максимальное значение 2 ² минус один (потому что мы должны представлять 0) - 4 294 967 295. Когда программа пытается присвоить переменной число за пределами ее возможного диапазона (например, 5 миллиардов), у нее заканчиваются биты и возникает проблема переполнения, а результат остается неопределенным и непредсказуемым.

Кстати, C99 представил заголовок ‹stdint.h›, который позволяет программисту явно указывать ширину целого числа. intN_t x = 10; определяет целое число со знаком x из N битов.

int8_t x = 10;       /* x is a 8-bit integer with value of 10 */
int32_t x = 100;     /* x is a 32-bit int with value of 100 */
uint64_t x;          /* declares x as a 64-bit unsigned int */

Это дает программисту больший контроль и лучшую переносимость между системами. Другая ситуация, когда явное указание целочисленных типов оказывается полезным, - это работа с char переменными. Является ли тип char подписанным или беззнаковым, определяется реализацией. Таким образом, использование подписанного int8_t вместо непредсказуемого char может обеспечить большую безопасность, предсказуемость и переносимость кода.

Надеюсь, теперь вы понимаете двоичную систему счисления, как положительные числа хранятся в памяти и почему существуют минимальные и максимальные возможные числа.

Теперь перейдем к вопросу о представлении отрицательных чисел в двоичном формате! Мы будем использовать меньшие числа с 4 битами для обсуждения.

Обозначение знака и величины

Концептуально простейшим форматом двоичного целочисленного представления является Нотация знак-величина. Самый левый бит обозначает знак, а остальные биты обозначают величину или размер числа.

0110 ==> +6                   0011 ==> +3
1110 ==> -6                   1011 ==> -3

Это прекрасно работает, но когда мы пытаемся двоичное сложение с записью знака-величины, недостатки становятся очевидными. Добавим +6 к -3:

Когда мы складываем 1 и 1, мы получаем 0, и значение 1 переносится в двоичном формате так же, как и в десятичном. Как показано в приведенном выше примере, когда добавляются +6 и -3, мы получаем двоичное представление +1, что явно не является правильным ответом. 👎

При использовании обозначения "знак-величина" также возникает проблема двойного представления числа 0.

0000  ==>  +0
1000  ==>  -0

Представьте, что вы пишете тест, который проверяет, больше ли число, чем 0. Разве не сложно проверить его как на положительный, так и на отрицательный 0? Излишне говорить, что Sign-Magnitude работает не очень хорошо.

Дополнение 1

В дополнении 1 отрицательные числа образуются путем взятия положительного числа и инвертирования каждого бита.

0001  ==>  +1           0110  ==>  +6           0111  ==>  +7
1110  ==>  -1           1001  ==>  -6           1000  ==>  -7

Это прекрасно работает. Теперь проверим эффективность бинарного сложения. В предыдущем примере, когда в старшем разряде есть переносимый бит, он просто волшебным образом отбрасывается и игнорируется. В дополнении 1, если есть переносимый бит, он переносится и добавляется к самому правому биту.

Вуаля! На этот раз двоичное сложение работает, как ожидалось, и мы получаем +3, когда складываем +6 и -3.

Но не слишком быстро! К сожалению, проблема двойного представления 0 все еще существует с дополнением 1.

0000  =>  +0
1111  =>  -0

Есть 16 возможных значений, которые могут быть представлены 4 битами, 7 положительными числами, 7 отрицательными числами и 2 нулями. Предположим, проблема двойного представления решена, что делать с дополнительным значением? Оно должно быть либо положительным, либо отрицательным числом.

Дополнение 2

Дополнение до 2 аналогично дополнению до 1, но мы добавляем 1 к перевернутому числу.

0011  ==>  +3                        0110  ==>  +6
1100  ==>  -3 /* 1’s complement */   1001  ==>  -6 /* 1’s */
1101  ==>  -3 /* 2’s complement */   1010  ==>  -6 /* 2’s */

Как и раньше, давайте добавим +6 и -3 для проверки двоичного сложения. Для дополнения 2 мы автоматически отбрасываем переносимый бит, как мы это делали в сложении Sign-Magnitude. Мы должны получить +3 за ответ.

Теперь о двух нулях:

0000  ==>  +0       1111  ==>  -0  /* in 1’s complement */
1111
+  1              /* add 1 to convert to 2’s complement */
0000  ==>  -0     /* they are the same!! */

Дополнение 2 решает проблему двойного представления 0!

Что представляет собой дополнительное значение, которое раньше представляло -0? И как мы можем быстро определить значение отрицательного числа в дополнении до 2?

Первая таблица представляет 16 возможных значений 4-битного целого числа без знака, а вторая таблица представляет возможные значения 4-битного целого числа со знаком.

Двоичное представление положительных чисел одинаково, независимо от того, является ли целое число со знаком или без знака.

В дополнении к единице -1 было представлено как1110, что эквивалентно 14 как целое число без знака. В дополнении до 2 -1 представляется как 1111, что эквивалентно 15 как целое число без знака. Как видите, мы расширили весь диапазон вниз на 1, что решает проблему двух нулей, а также позволяет нам представлять -8, тогда как раньше мы могли представлять только до -7.

Краткое руководство по быстрому вычислению значения отрицательного 4-битного числа:

  • Найдите беззнаковый эквивалент отрицательного числа (например: 1010 подписанный -6 становится беззнаковым 10 в двоичном формате)
  • Возьмите этот эквивалент (10 в нашем примере) и вычтите его на 16 (2 ^ n) (n - разрядность).
  • 10 — 16 == -6

Теперь вы лучше понимаете двоичную систему и то, как компьютер кодирует отрицательные числа! В то время как мы обсуждали Sign-Magnitude и Дополнение 1, почти все компьютерные архитектуры сегодня используют Дополнение 2. Если у вас есть дополнительные вопросы или вам нужны разъяснения, не стесняйтесь оставлять комментарии ниже или писать мне в твиттере @hizinzi!

Источники

Http://core.ecu.edu/csci/wirthj/Basen/signBin-c.html

Https://www.ibm.com/support/knowledgecenter/en/SSLTBW_2.3.0/com.ibm.zos.v2r3.bpxbd00/stdinth.htm

Https://www.youtube.com/watch?v=lKTsv6iVxV4

Https://stackoverflow.com/questions/9834747/reasons-to-use-or-not-stdint

Http://www.cs.uwm.edu/classes/cs315/Bacon/Lecture/HTML/ch04s11.html