Представление целых и вещественных чисел. Переполнение

Почему у целого числа есть «потолок», а у дробного — погрешность, заложенная в самом способе хранения.

Разрядная сетка — фиксированное число битов, отведённое под хранение числа; именно её конечность порождает и переполнение, и погрешности.

Зачем это нужно

Программисты-новички уверены, что числа в компьютере — «настоящие», как в математике. На самом деле любое число живёт в ограниченной разрядной сетке, и это объясняет целый класс багов: счётчик, который внезапно стал отрицательным; сравнение 0.1 + 0.2 == 0.3, которое возвращает False; деньги, которые «теряются» на копейках. Понимание того, как хранятся целые и вещественные числа, отличает инженера от того, кто просто пишет код наугад.

Корень всех этих явлений один: память конечна. Под каждое число отводится фиксированное число битов — байт, два, четыре, восемь. В математике числовая прямая бесконечна и непрерывна, а в компьютере она превращается в конечный набор «засечек». Между засечками значений просто нет, а за крайними засечками — обрыв. Из-за «обрыва» возникает переполнение (число выходит за границу и заворачивается), а из-за «промежутков между засечками» — погрешность вещественных чисел (нужное значение попадает между представимыми, и его округляют до ближайшего). Эти две беды — две стороны одной медали под названием «конечная разрядная сетка». Дальше в уроке мы разберём каждую, но держите в голове эту общую картину: всё, что кажется странным в компьютерной арифметике, объясняется тем, что бесконечную математику пытаются уместить в конечную память.

Целые без знака: считаем сколько влезает

Если под число отвели n бит и знак не нужен, поместятся значения от 0 до 2ⁿ − 1. В одном байте (8 бит) — от 0 до 255. Это и есть диапазон яркости цвета. Формула «2 в степени количества бит» — главная в этой теме:

for bits in (8, 16, 32):
    total = 2 ** bits
    print(f"{bits:>2} бит: {total} значений, без знака от 0 до {total - 1}")

Вывод:

 8 бит: 256 значений, без знака от 0 до 255
16 бит: 65536 значений, без знака от 0 до 65535
32 бит: 4294967296 значений, без знака от 0 до 4294967295

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

Как хранить минус, если есть только нули и единицы? Можно было бы отвести один бит под знак, но тогда возникает «минус ноль» и усложняется сложение. Компьютеры используют изящный дополнительный код: старший бit означает знак, а отрицательное число −x представляется как 2ⁿ − x. Тогда сложение положительных и отрицательных идёт по одним и тем же правилам — процессору не нужна отдельная схема вычитания.

Для 8-битного числа со знаком диапазон от −128 до +127. Соберём дополнительный код вручную и посмотрим на биты:

def twos_complement(x, bits=8):
    # представление x в дополнительном коде на заданном числе бит
    if x < 0:
        x = (1 << bits) + x        # 2^bits - |x|
    return format(x, "0" + str(bits) + "b")

for x in (5, -5, 127, -128, -1):
    print(f"{x:>5} → {twos_complement(x)}")

Вывод:

    5 → 00000101
   -5 → 11111011
  127 → 01111111
 -128 → 10000000
   -1 → 11111111

Обратите внимание: −1 — это все единицы, а старший бит у всех отрицательных равен 1. Это и есть «знаковый» бит.

Переполнение: когда число «заворачивается»

Что будет, если к максимуму 8-битного беззнакового числа (255) прибавить 1? Результат 256 не помещается в 8 бит, старший перенос «выпадает», и остаётся 0. Число переполнилось и «завернулось» по кругу. В реальных языках с фиксированными типами (C, Java, регистры процессора) это источник серьёзных багов; знаменитый пример — счётчики, которые внезапно обнуляются или становятся отрицательными.

В самом Python целые «резиновые» и растут без предела, поэтому переполнение мы смоделируем, явно ограничив разрядность маской:

MASK8 = 0xFF            # 8 единиц: оставляет только младшие 8 бит

a = 255
print("255 + 1 в 8-битной сетке =", (a + 1) & MASK8)   # заворот в 0
print("200 + 100 =", (200 + 100) & MASK8)              # 300 не влезает

# знаковая интерпретация того же байта
def as_signed(byte):
    return byte - 256 if byte > 127 else byte

print("байт 200 как знаковое:", as_signed(200))

Вывод:

255 + 1 в 8-битной сетке = 0
200 + 100 = 44
байт 200 как знаковое: -56

Вещественные числа: плавающая запятая

Дробные числа хранят в формате с плавающей запятой: число записывают как мантисса × 2^порядок, примерно как в науке пишут 6.02·10²³. Под мантиссу и порядок отведено фиксированное число бит (стандарт IEEE 754: 64 бита для типа double). Отсюда два следствия: огромный диапазон значений — и ограниченная точность. Не каждая десятичная дробь представима точно в двоичной, ровно как 1/3 не записывается конечной десятичной дробью.

Поэтому знаменитое:

a = 0.1 + 0.2
print("0.1 + 0.2 =", a)
print("равно 0.3?", a == 0.3)
print("разница  =", a - 0.3)
# правильное сравнение дробных — с допуском
import math
print("почти равно?", math.isclose(a, 0.3))

Вывод:

0.1 + 0.2 = 0.30000000000000004
равно 0.3? False
разница  = 5.551115123125783e-17
почти равно? True

Это не ошибка Python — так устроена двоичная плавающая запятая в любом языке. Вывод: вещественные числа нельзя сравнивать через ==; для денег используют целые копейки или специальные десятичные типы.

Попробуй сам

Проверьте границы знакового байта и посмотрите, как ведёт себя переполнение прямо на грани диапазона.

MASK8 = 0xFF
def as_signed(b):
    return b - 256 if b > 127 else b

print("Максимум знакового байта:", as_signed(127))
print("127 + 1 →", as_signed((127 + 1) & MASK8), "(переполнение!)")
print("Минимум знакового байта:", as_signed(128))

Вывод:

Максимум знакового байта: 127
127 + 1 → -128 (переполнение!)
Минимум знакового байта: -128

Частые ошибки

  • Считают диапазон знакового числа симметричным. Для 8 бит это −128…+127: отрицательных на одно больше, потому что ноль «занимает» положительную половину.
  • Сравнивают дробные через ==. Используйте math.isclose или сравнение модуля разности с малым ε.
  • Думают, что переполнение — редкость. В языках с фиксированными типами это повседневный риск; в Python целые не переполняются, но при работе с байтами/масками — вполне.
  • Путают точность и диапазон. Плавающая запятая даёт большой диапазон, но конечную точность мантиссы.

Итоги

  • n бит без знака хранят значения 0…2ⁿ−1; со знаком (дополнительный код) — от −2^(n−1) до 2^(n−1)−1.
  • Дополнительный код: −x = 2ⁿ − x, старший бит — знаковый; вычитание сводится к сложению.
  • Переполнение — «заворот» числа при выходе за разрядную сетку; в Python моделируется маской.
  • Вещественные хранятся как мантисса×2^порядок: большой диапазон, но конечная точность — отсюда 0.1+0.2≠0.3.
Проверьте себя
1. Сколько различных значений можно хранить в 10 битах?
A100
B1000
C1024
D512
2. Каков диапазон 8-битного целого числа со знаком (дополнительный код)?
Aот -127 до 127
Bот -128 до 127
Cот 0 до 255
Dот -128 до 128
3. Почему в Python 0.1 + 0.2 == 0.3 даёт False?
AЭто баг интерпретатора Python
BДесятичные дроби 0.1 и 0.2 не представимы точно в двоичной плавающей запятой
CPython округляет результат до целого
DНужно было писать 0.1 + 0.2 == 0.30
Поддержать проект