C++

Системи числення та двійкова арифметика

Що таке системи числення, як працюють двійкова, вісімкова та шістнадцяткова системи, переведення між ними, та арифметичні операції у двійковій системі — база для розуміння бітових операцій у C++.

Системи числення та двійкова арифметика

Чому програмісту важливо знати двійкову арифметику

Ви вже вмієте оголошувати змінні, писати цикли, використовувати функції. Але ось питання: що насправді відбувається, коли ви пишете int x = 42? Яке значення зберігається в пам'яті? Чому int займає 4 байти, а char — 1? Чому результат 255 + 1 у типі uint8_t дорівнює 0? Чому побітові операції (&, |, ^, >>, <<) такі потужні і де вони використовуються?

Відповідь на всі ці питання починається з одного фундаментального факту: комп'ютер зберігає і обробляє дані виключно у двійковій системі числення — послідовностях нулів і одиниць. Усе інше — це рівні абстракції над цим фактом.

Ця стаття — теоретичне підґрунтя для наступних тем: бітові операції, маски, прапорці, представлення від'ємних чисел у пам'яті. Без розуміння двійкової арифметики ці теми залишаться «магією».

Що таке система числення

Система числення (numeral system) — це спосіб запису і представлення чисел за допомогою символів (цифр) відповідно до певних правил.

Ключовий параметр будь-якої позиційної системи числення — основа (base, або radix): кількість різних символів (цифр), що використовуються. У позиційній системі значення цифри залежить від її позиції (розряду) у записі числа.

Загальна формула для числа у системі з основою B:

Число = dₙ × Bⁿ + dₙ₋₁ × Bⁿ⁻¹ + ... + d₁ × B¹ + d₀ × B⁰

де dᵢ — цифра на позиції i, B — основа системи.

Ми користуємось цим принципом щодня, навіть не замислюючись. Наприклад, число 342 у десятковій системі:

342 = 3 × 10² + 4 × 10¹ + 2 × 10⁰
    = 3 × 100 + 4 × 10  + 2 × 1
    = 300     + 40       + 2
    = 342

Саме так само будується будь-яка інша позиційна система — лише основа інша.


Системи числення у програмуванні

У програмуванні регулярно зустрічаються чотири системи числення:

Десяткова (Base 10)

  • Основа: 10
  • Цифри: 0 1 2 3 4 5 6 7 8 9
  • Звична людині
  • У C++: звичайний запис 42, 255, 1000

Двійкова (Base 2)

  • Основа: 2
  • Цифри: 0 1
  • Нативна для комп'ютера
  • У C++ (C++14): префікс 0b0b101010

Вісімкова (Base 8)

  • Основа: 8
  • Цифри: 0 1 2 3 4 5 6 7
  • Права UNIX, дозволи файлів
  • У C++: префікс 0052

Шістнадцяткова (Base 16)

  • Основа: 16
  • Цифри: 0–9 та A B C D E F
  • Адреси пам'яті, кольори, байти
  • У C++: префікс 0x0x2A

Таблиця відповідностей

ДесятковаДвійковаВісімковаШістнадцяткова
0000000
1000111
2001022
3001133
4010044
5010155
6011066
7011177
81000108
91001119
10101012A
11101113B
12110014C
13110115D
14111016E
15111117F

Двійкова система числення

Чому комп'ютер «думає» двійково

Фізична природа комп'ютера — транзистор, який може перебувати в одному з двох станів: провідний (є струм → 1) або непровідний (немає струму → 0). Один такий стан — це біт (bit, binary digit). Вісім бітів утворюють байт (byte).

Двійкова система — природна для цієї фізики. Будь-яке число або символ, будь-яка команда процесора, будь-яка адреса в пам'яті — все це зрештою зводиться до послідовності нулів і одиниць.

Читання двійкового числа

Двійкове число 1011 0110 читається так (справа ліворуч, позиції 0..7):

1 0 1 1   0 1 1 0
↑ ↑ ↑ ↑   ↑ ↑ ↑ ↑
7 6 5 4   3 2 1 0  ← позиція (степінь двійки)

= 1×2⁷ + 0×2⁶ + 1×2⁵ + 1×2⁴ + 0×2³ + 1×2² + 1×2¹ + 0×2⁰
= 128   + 0    + 32   + 16   + 0    + 4    + 2    + 0
= 182

Переведення з десяткової у двійкову

Метод послідовного ділення на 2 з записом залишків:

182 ÷ 2 = 91  залишок 0
 91 ÷ 2 = 45  залишок 1
 45 ÷ 2 = 22  залишок 1
 22 ÷ 2 = 11  залишок 0
 11 ÷ 2 = 5   залишок 1
  5 ÷ 2 = 2   залишок 1
  2 ÷ 2 = 1   залишок 0
  1 ÷ 2 = 0   залишок 1
             ↑
       читати знизу вгору: 1011 0110

Перевірка: 0b10110110 у C++ — це 182. ✅


Шістнадцяткова система числення

Чому шістнадцяткова — це «стислий двійковий»

Шістнадцяткова система особливо зручна для програмістів, тому що одна шістнадцяткова цифра точно відповідає чотирьом бітам (ніббл). Це робить запис бінарних даних у 4 рази компактнішим без втрати точності.

Двійкове:       1011   0110
Групуємо по 4:  1011 | 0110
Переводимо:      B   |  6
Шістнадцяткове: 0xB6

Саме тому адреси пам'яті, кольори (#FF5733), маски бітів і вміст байтів у hex-дампах записують у шістнадцятковій системі — це зручно для читання людиною і пряме відображення бінарного вмісту.

Переведення: hex ↔ десяткова

З шістнадцяткової в десяткову:

0xFF = 15×16¹ + 15×16⁰ = 240 + 15 = 255
0x2A = 2×16¹  + 10×16⁰ = 32  + 10 = 42

З десяткової в шістнадцяткову — метод ділення на 16:

255 ÷ 16 = 15  залишок 15 (F)
 15 ÷ 16 = 0   залишок 15 (F)
                    ↑ знизу вгору: FF

Запис у C++

NumberSystems.cpp
#include <iostream>

int main()
{
    int decimal     = 182;
    int binary      = 0b10110110; // C++14
    int octal       = 0266;
    int hexadecimal = 0xB6;

    // Усі чотири змінні містять одне й те саме значення!
    std::cout << decimal     << "\n"; // 182
    std::cout << binary      << "\n"; // 182
    std::cout << octal       << "\n"; // 182
    std::cout << hexadecimal << "\n"; // 182

    // Виведення у різних форматах
    std::cout << std::hex << decimal << "\n"; // b6
    std::cout << std::oct << decimal << "\n"; // 266
    std::cout << std::dec << decimal << "\n"; // 182

    return 0;
}
./NumberSystems
$ ./NumberSystems
182
182
182
182
b6
266
182
Execution finished with exit code 0.
Компілятор C++ зберігає значення в пам'яті однаково незалежно від системи числення у вихідному коді. 0b10110110, 0266, 0xB6 і 182 — це чотири різних записи одного й того самого числа. Вибір запису — лише питання зручності читання.

Вісімкова система числення

Вісімкова система використовує цифри від 0 до 7. Сьогодні вона найчастіше зустрічається у правах доступу до файлів у UNIX/Linux:

chmod 755 script.sh

Тут 755 — три вісімкові цифри, кожна з яких задає права для власника, групи і всіх інших. Чому вісімкова? Тому що три біти (rwx) — саме стільки потрібно для кодування трьох прав — дають 8 комбінацій (000..111), що точно відповідає одній вісімковій цифрі.

7 = 111 (двійк.) = rwx (read + write + execute)
5 = 101 (двійк.) = r-x (read + execute)
4 = 100 (двійк.) = r-- (тільки read)
У C++ вісімковий літерал записується з ведучим нулем: 052 — це 42, не п'ятдесят два. Це класична пастка: int x = 052 і int x = 42різні значення. Ведучий нуль у числовому літералі — знак вісімкової системи.

Арифметика в різних системах числення

Загальний принцип: перенесення розряду

Додавання в будь-якій системі числення працює за одним принципом: коли сума цифр у розряді досягає або перевищує основу системи, виникає перенесення (carry) в наступний розряд.

  347
+ 285
-----
  632

Одиниці: 7+5=12 → пишемо 2, переносимо 1
Десятки: 4+8+1=13 → пишемо 3, переносимо 1
Сотні:   3+2+1=6  → пишемо 6

Арифметичні операції у двійковій системі

Таблиця додавання бітів

Двійкове додавання ґрунтується на чотирьох базових випадках:

ABA + BРезультатПеренесення
00000
01110
10110
112 (→ 10₂)01

Якщо є ще й вхідне перенесення Cᵢₙ:

ABCᵢₙСумаCₒᵤₜ
00000
00110
01101
11001
11111

Покроковий приклад: 43 + 29

  43  →  0010 1011
+ 29  →  0001 1101
-----------------------
  72  →  0100 1000

Позиція 0: 1+1 = 10 → 0, перенос=1
Позиція 1: 1+0+1(пер) = 10 → 0, перенос=1
Позиція 2: 0+1+1(пер) = 10 → 0, перенос=1
Позиція 3: 1+1+1(пер) = 11 → 1, перенос=1
Позиція 4: 0+1+1(пер) = 10 → 0, перенос=1
Позиція 5: 1+0+1(пер) = 10 → 0, перенос=1
Позиція 6: 0+0+1(пер) = 01 → 1, перенос=0
Позиція 7: 0+0        = 00 → 0

Результат: 0100 1000 = 72 ✅

Двійкове віднімання

Метод 1 — пряме: аналогічно десятковому, але «позичаємо» 2 (а не 10):

  1011  (11)
- 0110  ( 6)
------
  0101  ( 5)

Позиція 0: 1-0=1
Позиція 1: 1-1=0
Позиція 2: 0-1 → позичаємо: 10-1=1, наступний розряд зменшується на 1
Позиція 3: 1-1-1(позика)=1-1-1 → позичаємо знову: 10+1-1-1=1

Метод 2 — через доповнення до двох (саме так роблять процесори — перетворюють A - B на A + (−B)). Це тема наступного розділу про представлення від'ємних чисел.

Двійкове множення

Таблиця множення бітів проста:

ABA × B
000
010
100
111

Множення двійкових чисел — це зсуви і додавання (саме так реалізують множення у «залізі»):

  1011  (11)
× 0101  ( 5)
----------
  1011  ← 1011 × 1 (позиція 0)
 0000   ← 1011 × 0 (позиція 1, зсув на 1)
1011    ← 1011 × 1 (позиція 2, зсув на 2)
----------
0110111 (= 55) ✅

Демонстрація у C++

BinaryArithmetic.cpp
#include <iostream>
#include <bitset>   // для двійкового виводу

int main()
{
    int a = 43;  // 0010 1011
    int b = 29;  // 0001 1101

    std::cout << "a      = " << std::bitset<8>(a) << " (" << a << ")\n";
    std::cout << "b      = " << std::bitset<8>(b) << " (" << b << ")\n";
    std::cout << "a + b  = " << std::bitset<8>(a + b) << " (" << a + b << ")\n";
    std::cout << "a - b  = " << std::bitset<8>(a - b) << " (" << a - b << ")\n";
    std::cout << "a * b  = " << std::bitset<16>(a * b) << " (" << a * b << ")\n";
    std::cout << "a / b  = " << std::bitset<8>(a / b)  << " (" << a / b  << ")\n";

    return 0;
}
./BinaryArithmetic
$ ./BinaryArithmetic
a = 00101011 (43)
b = 00011101 (29)
a + b = 01001000 (72)
a - b = 00001110 (14)
a * b = 0000000001000100110 (1247)
a / b = 00000001 (1)
Execution finished with exit code 0.
std::bitset<N> із заголовка <bitset> — зручний інструмент для виведення будь-якого цілого числа у двійковому форматі. N — кількість біт, що виводиться. Це не змінює саме число — лише формат виводу.

Переповнення: що відбувається, коли число «не вміщається»

Це наслідок двійкової арифметики, про який обов'язково треба знати:

Overflow.cpp
#include <iostream>
#include <cstdint>
#include <bitset>

int main()
{
    uint8_t maxValue = 255;  // 1111 1111
    uint8_t next     = maxValue + 1;

    std::cout << "255      = " << std::bitset<8>(maxValue) << "\n";
    std::cout << "255 + 1  = " << std::bitset<8>(next) << " (" << (int)next << ")\n";

    return 0;
}
./Overflow
$ ./Overflow
255 = 11111111
255 + 1 = 00000000 (0)
Execution finished with exit code 0.

255 + 1 у типі uint8_t дає 0 — перенесення «вивалюється» за межі восьми бітів і губиться. Це не баґ компілятора — це математика: 1111 1111 + 0000 0001 = 1 0000 0000, але дев'ятого біту в uint8_t немає.

Переповнення unsigned типів у C++ — визначена поведінка (результат береться за модулем 2ⁿ). Переповнення signed типів (int, long) — undefined behavior: компілятор може зробити що завгодно. Завжди думайте про діапазон значень при виборі типу.

Резюме

📐 Системи числення

  • Основа визначає кількість цифр і «вагу» кожного розряду
  • Програмісту потрібні: decimal (10), binary (2), octal (8), hex (16)
  • Одна hex-цифра = чотири біти; одна oct-цифра = три біти

🔢 Двійкова система

  • 1 біт → 2 стани; 8 біт → 256 значень; n біт → 2ⁿ значень
  • Переведення: ділимо на 2, записуємо залишки знизу вгору
  • У C++14: 0b10110110; виведення через std::bitset<N>

➕ Двійкова арифметика

  • Додавання: 1+1 = 10₂ (перенесення у наступний розряд)
  • Множення = зсуви + додавання
  • Переповнення unsigned: результат за модулем 2ⁿ

⚠️ Практична цінність

  • Побітові операції (&, |, ^, ~, <<, >>) — наступна стаття
  • Маски прапорців, права доступу, мережеві протоколи
  • Ефективне кодування кількох значень в одному int
Наступний крок — бітові операції: маски, прапорці, зсуви. Там двійкова арифметика перетворюється на потужний практичний інструмент.
Copyright © 2026