C++

Вказівники, const і масиви

Взаємодія вказівників із постійними значеннями (const). Вказівник на константу vs константний вказівник. Як масиви зводяться до вказівників. Символьні рядки.

Коли ми поєднуємо вказівники з модифікатором const або з масивами, синтаксис C++ може здатися дещо заплутаним. На цьому уроці ми розберемо всі можливі комбінації const та вказівників, а також дізнаємося, що масиви "під капотом" приховують ті самі вказівники.


Вказівники та const

До цього моменту всі наші вказівники були звичайними (неконстантними) і вказували на звичайні змінні. Ми могли:

  1. Змінити адресу, яку зберігає вказівник.
  2. Змінити значення за цією адресою.

Але що відбудеться, якщо ми спробуємо вказати на const змінну?

const int value = 7;
// int *ptr = &value; // ❌ ПОМИЛКА: Неможливо конвертувати const int* в int*

Якби C++ дозволив це, ми б змогли написати *ptr = 8; і тим самим змінити константу, зруйнувавши сам сенс слова "константа"! Тому компілятор це забороняє.

Існує три різні способи поєднати const із вказівниками. Синтаксис може здаватися складним, але є просте правило: читайте оголошення справа наліво, звертаючи увагу на те, де стоїть const відносно зірочки *.

1. Вказівник на константу (const int*)

Якщо const стоїть ДО зірочки, це означає: "можна змінювати сам вказівник (куди він вказує), але НЕ МОЖНА змінювати значення, на яке він вказує".

int a = 5;
const int b = 7;

// Читаємо справа наліво: ptr1 - це вказівник (*) на ціле число (int), яке є постійним (const)
const int *ptr1 = &b; // ✅ Можна вказувати на константу
const int *ptr2 = &a; // ✅ Можна вказувати і на звичайну змінну (але для нас вона тимчасово стане "read-only")

ptr1 = &a;            // ✅ ДОЗВОЛЕНО: Ми можемо перепризначити сам вказівник на іншу адресу
// *ptr1 = 10;        // ❌ ЗАБОРОНЕНО: Ми не можемо змінювати значення через цей вказівник
Цей тип вказівників найчастіше використовується в параметрах функцій, щоб гарантувати, що функція не змінить переданий об'єкт. (Хоча сьогодні для цього частіше використовують константні посиланняconst int&).

2. Константний вказівник (int* const)

Якщо const стоїть ПІСЛЯ зірочки, це означає: "вказівник назавжди прив'язаний до однієї адреси, але значення за цією адресою змінювати МОЖНА".

Такий вказівник обов'язково треба ініціалізувати при створенні:

int a = 5;
int b = 10;

// Читаємо справа наліво: ptr - це постійний (const) вказівник (*) на ціле число (int)
int * const ptr = &a;

*ptr = 8;   // ✅ ДОЗВОЛЕНО: a тепер дорівнює 8. Змінювати значення можна!
// ptr = &b; // ❌ ЗАБОРОНЕНО: ptr назавжди прив'язаний до адреси змінної a!

Це дуже схоже на поведінку звичайних посилань (int&).

3. Константний вказівник на константу (const int* const)

Ми можемо поставити const і до, і після зірочки. Це заблокує обидві можливості: "вказівник назавжди прив'язаний до адреси, і значення за нею змінювати не можна".

int a = 5;

// Постійний (const) вказівник (*) на ціле число (int), яке є постійним (const)
const int * const ptr = &a;

// ptr = &b;  // ❌ ЗАБОРОНЕНО
// *ptr = 10; // ❌ ЗАБОРОНЕНО

Підсумок по const

Щоб не плутатися, запам'ятайте цю табличку або просто подумки діліть оголошення по зірочці (*):

  • Все, що зліва від * — стосується значення в пам'яті.
  • Все, що справа від * — стосується самого вказівника (адреси).
ОголошенняЧи можна змінити ptr = &b?Чи можна змінити *ptr = 5?
int *ptr
const int *ptr
int * const ptr
const int * const ptr

Вказівники і масиви (Decay)

У C++ масиви фіксованого розміру (C-style arrays) та вказівники пов'язані дуже тісно. Подивіться на цей код:

int arr[4] = { 5, 8, 6, 4 };
cout << arr << "\n";

Рядок cout << arr замість того, щоб вивести вміст масиву, виведе шістнадцяткову адресу: 0x004BF968. Більше того, ця адреса абсолютно збігається з адресою першого елемента: cout << &arr[0].

У більшості випадків, коли ми використовуємо ім'я масиву, воно автоматично ("неявно") перетворюється на вказівник на свій перший елемент. Це явище називається Array Decay ("розпад масиву").

int arr[4] = { 5, 8, 6, 4 };
int *ptr = arr; // arr автоматично розпався на int* (вказівник на arr[0])

cout << *ptr << "\n";   // Вгадайте результат? Виведе 5!

Відмінності між масивом і вказівником

Попри "розпад", масив arr і вказівник ptrце не одне й те саме. Головна різниця випливає при використанні оператора sizeof.

  • sizeof(масив) повертає загальний об'єм масиву в байтах (кількість елементів * розмір елемента).
  • sizeof(вказівник) повертає лише розмір самої адреси (зазвичай 4 або 8 байт).
#include <iostream>

using namespace std;

int main()
{
    int arr[4] = { 5, 8, 6, 4 };
    int *ptr = arr;

    cout << "sizeof(arr): " << sizeof(arr) << "\n"; // 4 * 4 байти = 16
    cout << "sizeof(ptr): " << sizeof(ptr) << "\n"; // Розмір вказівника = 8 (на 64-бітних ОС)

    return 0;
}

Передача масивів у функції

Чому масиви не можна передавати у функції за значенням? Бо копіювати масив на 1 000 000 елементів — це дуже повільно! Тому C++ оптимізує цей процес: коли ви передаєте масив у функцію, він завжди передається як вказівник (Array Decay).

// Ці дві сигнатури абсолютно ідентичні для компілятора!
void printArray(int arr[]); 
void printArray(int *arr);

Масив скопіював лише свою 8-байтну адресу в параметр arr. Це швидко, але має два наслідки:

  1. Функція, змінюючи arr[i], змінює оригінальний масив (адже вона працює з оригіналом через вказівник).
  2. Оператор sizeof(arr) всередині функції поверне 8, а не розмір масиву. Звідси славнозвісна проблема: вказівник не знає, скільки в масиві елементів. Саме тому в С-стилі разом із масивом завжди передають параметр size.

Символьні константи рядків (String Literals)

Чи замислювалися ви коли-небудь, чим є текст у подвійних лапках?

cout << "Hello, World!";

Коли програма компілюється, рядок "Hello, World!" розміщується у спеціальному захищеному розділі оперативної пам'яті "Тільки для читання" (Read-Only Memory). А сам літерал "Hello, World!" неявно стає вказівником на константний символ (const char*), який вказує на літеру 'H' з цього захищеного розділу.

const char *name = "John"; // ✅ name - це вказівник на Read-Only рядок "John\0"
cout << name;              // Виведе John
Зауважте, що це кардинально відрізняється від масиву char.
char arrName[] = "John"; // Створює локальний масив і КОПІЮЄ туди літери.
arrName[0] = 'D';        // Можна змінювати! Стане "Dohn".

const char *ptrName = "John"; // Вказівник на Read-Only оригінал в пам'яті.
// ptrName[0] = 'D';     // ❌ ПОМИЛКА під час виконання! (Access Violation).

Чому cout обробляє char* інакше?

Ви могли помітити дивину: якщо вивести в cout вказівник int*, ми побачимо адресу (наприклад 0x00A1). Чому ж cout << name виводить слово "John", а не адресу символу 'J'?

Справа в тому, що творці бібліотеки iostream зробили для типу char*const char*) спеціальний виняток: замість виведення адреси, cout інтерпретує його як рядок (C-style). Він виводить символ за адресою, зміщує адресу на 1, виводить наступний символ, і так продовжує, доки не зустріне нуль-термінатор '\0'.

Іноді це призводить до кумедних багів. Якщо ви маєте масив символів без \0 і спробуєте вивести його адресу, або якщо ви спробуєте вивести адресу звичайного char:

char ch = 'R';
cout << &ch; // &ch має тип char*, тому cout подумає, що це рядок!

Базуючись на правилі вище, cout виведе літеру R, а потім продовжить читати пам'ять далі (виводячи «сміття»: значки, ієрогліфи), аж поки випадково не натрапить на нульовий байт.


Підсумок

const і вказівники

Читайте справа наліво щодо *. const int* — не можна змінювати значення. int* const — не можна змінювати, куди дивиться вказівник.

Array Decay

Ім'я масиву (arr) за замовчуванням "розпадається" (decay) і перетворюється на вказівник на найперший елемент &arr[0].

Передача масивів

Масиви передаються у функції виключно як вказівники, навіть якщо сигнатура має вигляд int arr[]. Розмір масиву (через sizeof) при цьому втрачається.

Символьні рядки

Літерали типу "Рядок" зберігаються у Read-Only пам'яті та представляють собою const char*. А от char[] — це локальна копія, яку можна мутувати.
Copyright © 2026