C++

Адресна арифметика

Додавання та віднімання з вказівниками (pointer arithmetic). Як масиви зберігаються в пам'яті. Синтаксичний цукор індексації масивів (array[i]). Ітерація за допомогою вказівників.

Ми вже знаємо, що вказівник — це число (адреса в пам'яті). Відповідно, з цими "числами" можна виконувати математичні операції. У С++ дозволено додавати та віднімати цілі числа від вказівників.

Це явище називається адресною арифметикою (або арифметикою вказівників). І працює вона не зовсім так, як звичайна шкільна математика.


Як працює адресна арифметика?

Припустімо, у нас є вказівник ptr, що зберігає адресу 0x0010. Здавалося б, математичний вираз ptr + 1 має дати результат 0x0011. Але це не так!

Вказівник знає тип даних, на який він вказує. Коли ми додаємо 1, компілятор розуміє це не як "перейди на 1 байт вперед", а як "перейди на 1 такий об'єкт вперед". Тому формула адресної арифметики виглядає так: нова_адреса = поточна_адреса + (число * розмір_типу_в_байтах).

Розглянемо на прикладі:

#include <iostream>

using namespace std;

int main()
{
    int value = 8;
    int *iPtr = &value;
    
    // Припустимо, iPtr має адресу 0x2CF9A4
    cout << iPtr     << "\n"; // 0x2CF9A4
    cout << iPtr + 1 << "\n"; // 0x2CF9A8 (+ 4 байти)
    cout << iPtr + 2 << "\n"; // 0x2CF9AC (+ 8 байт)
    cout << iPtr + 3 << "\n"; // 0x2CF9B0 (+ 12 байт)

    return 0;
}

Оскільки int зазвичай займає 4 байти, кожен крок вперед на +1 зміщує адресу рівно на 4 байти. Але якщо ми використаємо тип short (що займає 2 байти), крок зміниться:

short val = 8;
short *sPtr = &val;

// Припустимо, sPtr має адресу 0x2BFA20
cout << sPtr     << "\n"; // 0x2BFA20
cout << sPtr + 1 << "\n"; // 0x2BFA22 (+ 2 байти)
cout << sPtr + 2 << "\n"; // 0x2BFA24 (+ 4 байти)

Отже, компілятор всюди автоматично враховує sizeof(тип), щоб ми могли стрибати від об'єкта до об'єкта, не думаючи про їхній розмір у байтах.


Розташування елементів масиву в пам'яті

Ця математика здається дуже небезпечною, коли вказівник дивиться на одну змінну (value). Адже iPtr + 1 вкаже на якесь "сміття" після нашої змінної! Навіщо ж тоді це потрібно?

Головна (і фактично єдина) сфера застосування адресної арифметики — робота з масивами.

Масиви у С++ гарантовано розміщуються в оперативній пам'яті суцільним блоком "стик у стик". Переконаємось у цьому:

#include <iostream>

using namespace std;

int main()
{
    int arr[4] = { 7, 8, 2, 4 };
    
    cout << "arr[0] address: " << &arr[0] << "\n"; // Наприклад: 0x002CF6F4
    cout << "arr[1] address: " << &arr[1] << "\n"; // Тоді тут:  0x002CF6F8
    cout << "arr[2] address: " << &arr[2] << "\n"; // Тоді тут:  0x002CF6FC
    cout << "arr[3] address: " << &arr[3] << "\n"; // Тоді тут:  0x002CF700

    return 0;
}

Як бачимо, кожен елемент лежить чітко через 4 байти (розмір int) від попереднього.


Індексація масивів "розкрита"

Пам'ятаєте з минулого уроку, як назва масиву "розпадається" у вказівник на його перший елемент? Тобто arr дорівнює &arr[0].

Зведемо до купи три факти:

  1. arr — це вказівник на arr[0].
  2. Елементи масиву лежать послідовно.
  3. Додавання + n до вказівника бере адресу елемента на відстані n.

Тоді вираз arr + 1 дасть адресу arr[1]. А вираз arr + 2 дасть адресу arr[2].

А щоб отримати саме значення (а не просто адресу), нам достатньо розіменувати цей вказівник оператором *!

int arr[5] = { 7, 8, 2, 4, 5 };

cout << arr[1] << "\n";       // Виведе 8 (через індекс)
cout << *(arr + 1) << "\n";   // Виведе 8 (через арифметику вказівників!)

Дужки *(arr + 1) обов'язкові, оскільки оператор * має вищий пріоритет, ніж +. Без дужок *arr + 1 обчислиться як (значення arr[0]) + 1, тобто 7 + 1 = 8. Це збіглося з відповіддю, але якби ми хотіли другий елемент: *arr + 2 дало б 9 (7+2), а не потрібне 2.

!CAUTION Синтаксичний цукор! Секрет С++: оператор квадратних дужок [] — це не якась магічна інструкція процесора. Це звичайнісінький синтаксичний цукор! Коли ви пишете arr[n], компілятор перед збиранням програми автоматично перетворює цей рядок на *(arr + n).

Саме тому індексація масивів у C++ починається з нуля. Бо перший елемент має зміщення (офсет) нуль: *(arr + 0) — це те ж саме, що просто *arr.


Ітерація по масиву через вказівник

Знаючи, що [i] це просто замаскований + i, ми можемо переписувати цикли for для проходження по масиву, оперуючи безпосередньо вказівниками.

Це був єдиний можливий спосіб ітерації у ранніх версіях мови C. Сьогодні це радше історичний факт і спосіб для оптимізації в окремих "вузьких" місцях високопродуктивних бібліотек.

#include <iostream>

using namespace std;

int main()
{
    const int SIZE = 5;
    int arr[SIZE] = { 10, 20, 30, 40, 50 };

    // Звичайна ітерація через індекс:
    // for (int i = 0; i < SIZE; ++i) { cout << arr[i] << " "; }

    // Ітерація через адресну арифметику (вказівник):
    for (int *ptr = arr; ptr < arr + SIZE; ++ptr)
    {
        cout << *ptr << " ";
    }
    // Виведе: 10 20 30 40 50

    return 0;
}

Як працює цей цикл?

  1. int *ptr = arr — наш вказівник ptr стає на старт (показує на arr[0]).
  2. ptr < arr + SIZE — умова зупинки. arr + SIZE — це адреса, яка знаходиться відразу ПОЗА межами масиву. Поки ми її не досягли, цикл продовжується.
  3. ++ptr — операція інкремента на вказівнику рівнозначна ptr = ptr + 1. Вона пересуває стрілку вказівника на наступний елемент (на 4 байти вперед).
  4. *ptr — розіменування, щоб вивести число на екран.
Здебільшого використовуйте Range-based for loop (for(int x : arr)) або звичайну індексацію arr[i], оскільки вони читабельніші та менш вразливі до помилок виходу за межі пам'яті (SegFault). Але розуміти, як працює адресна ітерація під капотом, — дуже важливо для глибокого володіння C++.

Підсумок

🧮 Кроки арифметики

Вираз ptr + n зміщує адресу на відстань n * sizeof(Type) байтів. Це забезпечує безпечний перехід між елементами незалежно від їхнього розміру.

📦 Пам'ять масивів

Елементи масиву лежать у пам'яті суцільним блоком один за одним, що робить адресну арифметику можливою і швидкою.

🍬 Синтаксичний цукор

Доступ за індексом arr[n] — це лише зручна для читання обгортка над чистою адресною математикою *(arr + n).
Copyright © 2026