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

Двоичная система счисления: перевод, арифметика, дополнительный код Полезное

Двоичная система счисления — это позиционная система с основанием 2, в которой любое число записывается всего двумя цифрами: 0 и 1. Каждая такая цифра называется битом (binary digit). Разряды считаются справа налево, и вес каждого разряда — это степень двойки: 1, 2, 4, 8, 16 и так далее.

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

Почему в компьютерах именно два состояния

Разряд в памяти или на линии связи проще всего сделать двухуровневым: есть сигнал или нет, заряд выше порога или ниже. Два четко различимых уровня устойчивы к помехам, поэтому вся цифровая техника оперирует нулями и единицами. Это инженерное упрощение: физически там не «лампочка горит», а напряжение в диапазоне, которое схема трактует как 0 или 1.

Из этого не следует, что двоичная система «единственно правильная». Для человека она длинная и плохо читается: десятичное 100 — это уже 1100100. Поэтому рядом живут восьмеричная и шестнадцатеричная записи — они короче и переводятся в двоичную по группам разрядов.

Разряды и веса: как читать двоичное число

Самый правый разряд имеет номер 0, следующий влево — 1, затем 2 и так далее. Вес разряда номер k равен 2 в степени k.

Разряд k 7 6 5 4 3 2 1 0
Вес 2^k 128 64 32 16 8 4 2 1

Держите эту таблицу перед глазами — на ней строятся оба перевода.

Из двоичной в десятичную (по шагам)

Возьмем число 1011 в двоичной записи. Алгоритм:

  1. Пронумеруем разряды справа налево: цифры 1, 1, 0, 1 получают номера 0, 1, 2, 3.
  2. Каждую единицу заменим ее весом 2^k, нули пропустим.
  3. Сложим полученные веса.

Распишу для 1011:

разряд:   3   2   1   0
цифра:    1   0   1   1
вес:      8   4   2   1
вклад:    8 + 0 + 2 + 1 = 11

Значит, 1011 в двоичной = 11 в десятичной. Прием «пропускаем нули» работает потому, что нулевой разряд дает нулевой вклад: домножать вес на 0 незачем.

Проверю кодом на Python. Встроенная функция int принимает основание вторым аргументом:

b = "1011"
print(int(b, 2))   # 11

В консоли появится 11 — совпадает с расчетом руками.

Из десятичной в двоичную (по шагам)

Обратный перевод — деление на 2 с сбором остатков. Возьмем число 11.

  1. Делим на 2, записываем частное и остаток.
  2. Повторяем деление уже с частным, пока частное не станет 0.
  3. Выписываем остатки снизу вверх (от последнего к первому).

Прогон для 11:

11 / 2 = 5, остаток 1
 5 / 2 = 2, остаток 1
 2 / 2 = 1, остаток 0
 1 / 2 = 0, остаток 1

Читаем остатки снизу вверх: 1011. Получили те же 1011, что и в прямом переводе, — это хорошая самопроверка.

Кодом это делают встроенные функции. bin возвращает строку с префиксом 0b, format дает чистые цифры:

n = 11
print(bin(n))          # 0b1011
print(format(n, "b"))  # 1011

Вывод: сначала строка 0b1011, затем 1011.

Биты, байты и группировка разрядов

Бит — один двоичный разряд. Байт — группа из 8 бит; это минимальная адресуемая ячейка памяти в большинстве архитектур. В 8 битах помещается 2^8 = 256 разных комбинаций: без знака это числа от 0 до 255.

Отсюда удобный перевод в шестнадцатеричную запись: 4 бита — это ровно одна шестнадцатеричная цифра (0-F), а байт — две такие цифры. Поэтому программисты часто пишут байты в hex: так короче и нагляднее.

Двоичное (4 бита) Десятичное Hex
0000 0 0
0101 5 5
1010 10 A
1111 15 F

Чтобы перевести байт 11111011 в hex, разбиваем на две тетрады: 1111 и 1011. Это 15 и 11, то есть F и B. Итог: 0xFB.

Двоичная арифметика: сложение

Складывают двоичные числа столбиком, справа налево, ровно как десятичные, только перенос возникает уже при сумме 2. Правила разряда: 0+0=0, 0+1=1, 1+1=10 (пишем 0, переносим 1), 1+1+1=11 (пишем 1, переносим 1).

Сложим 1011 (11) и 0110 (6):

  1011
+ 0110
------
 10001

Разберу переносы справа налево: 1+0=1; 1+1=10 (0, перенос 1); 0+1+1=10 (0, перенос 1); 1+0+1=10 (0, перенос 1); наверх уходит 1. Результат 10001 = 16 + 1 = 17, и 11 + 6 действительно 17.

Двоичная арифметика: умножение

Умножение проще десятичного: частичное произведение — либо копия множимого (когда цифра множителя 1), либо нули (когда 0), со сдвигом по разряду. Умножим 101 (5) на 11 (3):

   101
 x  11
------
   101   (умножили на младшую 1)
+ 1010   (умножили на старшую 1, сдвиг на разряд)
------
  1111

1111 = 8 + 4 + 2 + 1 = 15, и 5 умножить на 3 — это 15.

Отрицательные числа: дополнительный код

Знак минуса в памяти отдельно не хранится — его кодируют самим набором бит. Стандартный способ — дополнительный код (two’s complement). Идея: старший бит фиксированной разрядности несет отрицательный вес. Для 8 бит веса разрядов такие:

Разряд 7 6 5 4 3 2 1 0
Вес -128 64 32 16 8 4 2 1

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

  1. Записываем модуль в двоичном виде: 5 = 00000101.
  2. Инвертируем все биты (0 меняем на 1 и наоборот): 11111010.
  3. Прибавляем 1: 11111011.

Проверим через веса старшего разряда: 11111011 = -128 + 64 + 32 + 16 + 8 + 2 + 1 = -5. Сходится.

Почему так удобно: вычитание сводится к сложению с дополнительным кодом, и отдельная схема вычитания процессору не нужна. Посчитаем 11 — 5 как 11 + (-5) в 8 битах:

  00001011   (11)
+ 11111011   (-5)
----------
1 00000110   (перенос из старшего разряда отбрасывается)

Оставшиеся 8 бит 00000110 = 6, и 11 — 5 равно 6. Перенос за пределы разрядности в дополнительном коде просто игнорируется — это часть механики, а не ошибка.

Важная граница: диапазон зависит от числа бит. Знаковый байт (8 бит) покрывает не -255..255, а от -128 до 127. Выход за этот предел дает переполнение, и результат «заворачивается». Поэтому разрядность всегда указывают явно.

Проверю дополнительный код кодом. В Python целые не ограничены 8 битами, поэтому маску 8 бит задаю через & 0xFF:

x = -5
print(format(x & 0xFF, "08b"))  # 11111011

Вывод 11111011 — тот самый набор бит, что я получил руками.

Тот же расчет на C++

В C++ работа с битами ближе к «железу»: std::bitset печатает разряды явно. Минимальный полный пример:

#include <iostream>
#include <bitset>

int main() {
    int n = 11;
    std::bitset<8> b(n);
    std::cout << b << "\n";            // 00001011
    std::cout << b.to_ulong() << "\n"; // 11

    signed char neg = -5;
    std::bitset<8> nb(static_cast<unsigned char>(neg));
    std::cout << nb << "\n";           // 11111011
    return 0;
}

Ожидаемый вывод в консоли построчно: 00001011, затем 11, затем 11111011. Первые две строки — прямой и обратный перевод числа 11, третья — дополнительный код -5, который совпал с расчетом на бумаге и с примером на Python.

Частая ошибка новичка

Типичный промах — при переводе десятичного в двоичное выписать остатки в том порядке, в каком они появились, сверху вниз. Для 11 это дало бы 1101 вместо 1011:

n, digits = 11, ""
while n > 0:
    digits += str(n % 2)   # дописываем остаток в конец
    n //= 2
print(digits)              # 1101 - неверный порядок

Вывод 1101 читается как 8 + 4 + 1 = 13, а не 11. Остатки нужно переворачивать — собирать в обратном порядке:

n, digits = 11, ""
while n > 0:
    digits = str(n % 2) + digits   # дописываем остаток в начало
    n //= 2
print(digits)                      # 1011 - верно

Теперь вывод 1011 = 11, ошибка устранена.

Выводы

  • Двоичная система позиционная, основание 2, вес разряда k равен 2^k; перевод в десятичную — сумма весов единичных разрядов.
  • Обратный перевод — деление на 2 с остатками, которые выписывают снизу вверх; порядок остатков легко перепутать.
  • Сложение, вычитание и умножение делаются столбиком, перенос возникает при сумме 2.
  • Отрицательные числа кодируют дополнительным кодом (инверсия + 1); вычитание тогда сводится к сложению, а диапазон зависит от разрядности (8 бит со знаком: -128..127).
  • Бит — один разряд, байт — 8 бит (256 комбинаций), тетрада бит удобно ложится на одну шестнадцатеричную цифру.

Где применяется / связь с практикой

Понимание битов и дополнительного кода нужно везде, где важна работа с памятью: битовые флаги и маски, сетевые протоколы, форматы файлов, целочисленное переполнение. В C и C++ это база: типы фиксированного размера, знаковые и беззнаковые целые, сдвиги и маски &, |, ^, <<, >> опираются ровно на разобранную здесь механику разрядов.

Освойте тему на практике

Если хочется дойти от «читаю биты руками» до уверенной работы с ними в коде, разобрать это системно помогает курс C++ Basic. Посмотреть формат и уровень занятий можно на открытых уроках Otus — там разбирают конкретные задачи вживую.

FAQ

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

Сколько разных значений помещается в n бит?
Ровно 2 в степени n. В 1 бите — 2 значения, в 4 битах — 16, в 8 битах (байте) — 256. Для знаковых чисел то же количество комбинаций делится между отрицательными и неотрицательными.

Зачем нужны восьмеричная и шестнадцатеричная записи, если техника работает в двоичной?
Это компактная запись тех же битов для человека: 3 бита — одна восьмеричная цифра, 4 бита — одна шестнадцатеричная. Читать 0xFB проще, чем 11111011, а перевод между ними механический.

OTUS Журнал
Скидка 5% 12-19 октября на курсы (popup)
Бесплатные открытые уроки (поп-ап)