Дополнительный код: представление отрицательных чисел
Почти все современные процессоры хранят целые числа в дополнительном коде (two's complement). Это не произвольный выбор: именно он позволяет складывать положительные и отрицательные числа одной и той же схемой, без специального флага знака и без дополнительных схем вычитания. Ниже разберём, как устроено это представление, почему диапазон чисел несимметричен, как перевести любое отрицательное число и где чаще всего ошибаются студенты. Для наглядности используй калькулятор ниже: он показывает двоичный и шестнадцатеричный код, вклад каждого разряда и положение числа на «кольце» кодов.
Почему не просто добавить бит знака
Наивная идея: взять двоичное представление числа и добавить к нему отдельный бит знака (0 - плюс, 1 - минус). Называется такой формат «прямой код». Проблема: у нуля появляются два представления - и - и сложение с отрицательными числами требует отдельной схемы с вычитателем. Дополнительный код решает обе проблемы разом.
Ключевая идея: старший разряд (MSB) получает отрицательный вес , остальные разряды сохраняют обычные положительные веса . Значение числа по -битному слову определяется формулой:
Именно поэтому слово в 8 битах означает не , а : единица в знаковом разряде вносит вес , а все остальные биты нулевые.
Диапазон и несимметричность
В -битном дополнительном коде можно представить числа от до :
Диапазон несимметричен: отрицательных чисел на одно больше, чем положительных. Так происходит потому, что ноль «занимает» место в положительной половине. Для 8 бит: от до , для 16 бит: от до , для 32 бит: от до .
Эта несимметрия имеет практические последствия. В языке C стандарт гарантирует, что тип int вмещает как минимум , а на большинстве 32-битных платформ - . Функции наподобие abs() и Math.abs() возвращают неожиданный результат для минимального значения, потому что его положительный аналог не помещается в тот же тип.

Показательный пример: в 8 битах записывается как (все единицы), а - как (только знаковый бит). Для не существует положительного двойника той же разрядности: уже не помещается в 8-битный диапазон и требует 9 битов.
Как переводить число в дополнительный код
Три эквивалентных способа.
Способ 1: через вычитание из . Для отрицательного числа его -битный дополнительный код - это беззнаковое значение .
Пример: в 8 битах .
Способ 2: инверсия плюс единица. Берём абсолютное значение , записываем его в двоичном виде, инвертируем все биты (заменяем 0 на 1 и наоборот), добавляем 1.
.
Способ 3: по весам разрядов. Строим единственную комбинацию битов , при которой . Знаковый бит обязательно 1 (число отрицательное), тогда , то есть . Итог: .
Все три способа дают одинаковый результат.
Обратное преобразование: из кода в число
Чтобы прочитать знаковое значение из дополнительного кода, можно воспользоваться той же формулой весов: если старший бит равен 1, число отрицательное; его величина считается как , где - беззнаковая интерпретация того же слова.
Пример: (без знака). Знаковый бит единица, значит .
Этот алгоритм используют и процессоры, и компиляторы при разыменовании. Когда ты объявляешь переменную как знаковый тип - компилятор читает старший бит как отрицательный вес. Беззнаковый тип той же разрядности использует тот же битовый паттерн, но интерпретирует старший бит как обычный положительный вес . Именно поэтому приведение (знаковый int8) к беззнаковому uint8 даёт : те же биты , другая интерпретация.
Сложение работает «само»
Главное преимущество дополнительного кода: сложение двух N-битных чисел с любыми знаками выполняется одной схемой обычного двоичного сумматора, а результирующий N-битный код даёт правильный ответ при отсутствии переполнения.
Перенос из старшего разряда отбрасывается. Вычитание реализуется как , то есть сложение с дополнительным кодом . Чтобы получить , достаточно инвертировать биты и прибавить 1 - ровно та же операция, что и при переводе в дополнительный код.
Это свойство позволяет процессору использовать единственный сумматор для всей целочисленной арифметики. В прямом коде потребовалось бы отдельное устройство для вычитания, отдельное для инвертирования знака и логика выбора между операциями. Дополнительный код устраняет эту сложность на аппаратном уровне, что стало одной из главных причин его повсеместного принятия в 1960-х годах.
Переполнение
Переполнение происходит, когда истинный результат операции выходит за диапазон -битного представления. Процессор обнаруживает его по флагу V (overflow): он устанавливается, когда знак сложения двух чисел с одинаковыми знаками не совпадает со знаком результата.
Пример: в 8 битах , но , поэтому код интерпретируется как - неверный знаковый результат с установленным флагом V. Знаковый бит у обоих операндов равен 0 (оба положительны), а у результата равен 1 (отрицательный) - именно это несоответствие и сигнализирует переполнение.
Правило детекции переполнения при сложении: если оба слагаемых положительны и сумма отрицательна (или оба отрицательны и сумма положительна) - произошло переполнение. При вычитании и при сложении разных знаков переполнение невозможно.
Частые ошибки
- Инвертировать биты, но забыть прибавить единицу. Результат после инверсии - обратный код (ones' complement), а не дополнительный. Без +1 значение на 1 больше нужного.
- Перепутать диапазон. В 8 битах минимум , максимум , а не . Попытка записать в
int8_t- UB в C/C++ и переполнение в аппаратной арифметике. - Читать код без учёта разрядности. Код в 4 битах означает , но в 8 битах - это . Разрядность контекста критична.
- Вычислять дополнение от нуля как . Число 0 в дополнительном коде - ровно нулей; специального кода для нет.
- Переполнение при смене знака . Это единственное число, для которого операция «взять противоположное» не работает в той же разрядности: не помещается в 8-битный int.
FAQ
Чем дополнительный код отличается от прямого и обратного? Прямой код хранит знак отдельным битом, имеет два нуля и требует специального вычитателя. Обратный код получается инверсией всех битов модуля; у него тоже два нуля ( и ), и сложение требует циклического переноса. Дополнительный код = обратный + 1, один ноль, обычный двоичный сумматор. Именно поэтому все современные архитектуры (x86, ARM, RISC-V) используют дополнительный код для знаковых целых.
Как узнать знак числа по дополнительному коду, не разворачивая вычисления? Достаточно посмотреть на старший (левый) бит: 0 - число неотрицательное, 1 - отрицательное. Это прямое следствие того, что старший бит имеет отрицательный вес .
Почему отрицательных чисел на одно больше, чем положительных? Ноль требует своего представления. Все возможных кодов распределяются так: один под ноль, под положительные и под отрицательные. «Лишнее» отрицательное число - это , у которого нет симметричного двойника в той же разрядности.
Коротко
Дополнительный код - стандартный способ хранить знаковые целые в компьютере: старший разряд получает отрицательный вес , остальные - обычные положительные веса. Диапазон N-битного числа: от до . Отрицательное число получают тремя путями: формулой , инверсией плюс единица или подбором по весам. Главное преимущество формата - сложение и вычитание реализуются единой схемой без дополнительной логики знака.
Читайте также

Кодирование текста в ASCII: коды, биты и объём
Как работает кодирование текста в ASCII: что такое код символа, как перевести букву в двоичный и шестнадцатеричный вид, сколько бит и байт занимает строка и чем ASCII отличается от Unicode.

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

Абстрактный класс и интерфейс: в чём отличие
Абстрактный класс и интерфейс: чем отличаются в ООП, когда наследовать поведение, а когда задавать контракт, как выбрать на примерах Java, C# и Python.