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): префікс 0b → 0b101010

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

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

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

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

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

ДесятковаДвійковаВісімковаШістнадцяткова
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