JavaRush /Курси /C++ SELF /const T*, T* const, const T* const: як застосовувати

const T*, T* const, const T* const: як застосовувати

C++ SELF
Рівень 40 , Лекція 3
Відкрита

1. Чому const із вказівниками так часто плутає

Якщо вам уже здавалося, що const інколи виглядає так, ніби «прилипло десь ліворуч, а сенс — праворуч», це нормально. Вказівники додають другий рівень складності: сам вказівник — це теж значення, і його можна змінювати. Тому одразу виникають два різні запитання: «чи можна змінювати обʼєкт?» і «чи можна перепризначати саму адресу?».

Уявіть вказівник як папірець з адресою. На папірці написано: «вулиця Шевченка, будинок 12». Ви можете або змінити адресу на папірці, або піти за цією адресою й переставити меблі в квартирі, тобто змінити обʼєкт. const може заборонити одну дію, іншу або відразу обидві.

Дві точки застосування: *p і p

Коли ми пишемо int x = 10;, x — це змінна-значення. Коли ми пишемо int* p = &x;, p теж є змінною-значенням, тільки зберігає не число 10, а адресу.

Міні-модель, яку корисно тримати в голові:

  • *p — це «те, що лежить за адресою».
  • p — це «сама адреса» (значення вказівника).

Далі завжди ставимо два запитання.

Запитання A. Чи можна змінювати обʼєкт через *p?
Запитання B. Чи можна змінювати сам вказівник (робити p = &other)?

Якщо тримати в голові ці два запитання, то весь набір const T*, T* const, const T* const перетворюється на зрозумілу схему, а не на заклинання.

2. Три базові форми: const T*, T* const, const T* const

const T* (або T const*): вказівник на константу

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

Запис const int* p читається так: «p вказує на const int». Тобто через p не можна змінювати значення. Але сам p — звичайна змінна, тому його можна перепризначити на іншу адресу.

#include <iostream>

int main() {
    int a = 1;
    int b = 2;

    const int* p = &a;  // вказівник на const int
    // *p = 10;          // не можна: обʼєкт лише для читання
    p = &b;             // можна: сам вказівник не const

    std::cout << *p << '\n'; // 2
}

Важливо: при цьому a не мусить бути константою. Це звичайний int. Просто через p ви добровільно «обмежуєте можливість запису». Це дуже корисна ідея: не просити права на зміну, якщо вони не потрібні.

T* const: константний вказівник

Тепер перевернімо ситуацію: інколи потрібно, щоб вказівник завжди дивився в одне місце, але обʼєкт за цією адресою можна було змінювати.

Тоді ми робимо p константним, а обʼєкт — ні: int* const p.

Ключова деталь: const стоїть після *, тобто стосується самого вказівника — змінної, що зберігає адресу.

#include <iostream>

int main() {
    int a = 1;
    int b = 2;

    int* const p = &a;  // const-вказівник на int
    *p = 10;            // можна: обʼєкт не const
    // p = &b;           // не можна: p не можна перепризначити

    std::cout << a << '\n'; // 10
}

Такий варіант часто зручний усередині реалізації, тобто всередині функції або методу, щоб захиститися від випадкового перепризначення адреси.

const T* const: не можна змінювати ні обʼєкт, ні адресу

Це найсуворіший варіант: не можна змінити ні обʼєкт за адресою, ні саму адресу. Тобто «вказівник зафіксований, а доступ — лише для читання».

#include <iostream>

int main() {
    int a = 5;
    int b = 7;

    const int* const p = &a;
    // *p = 1;  // не можна: обʼєкт const
    // p = &b;  // не можна: p const

    std::cout << *p << '\n'; // 5
}

З практичного погляду це особливо зручно в складних місцях коду, де ви хочете гарантувати, що змінна p не піде на іншу адресу і через неї ви точно не зіпсуєте дані.

3. Таблиця: що можна, а що не можна

Ось таблиця, яку зручно подумки діставати щоразу, коли ви бачите * const:

Запис типу Як читається за змістом Можна змінювати *p Можна робити p = ...
T*
вказівник на
T
так так
const T* (T const*)
вказівник на
const T
ні так
T* const
const
-вказівник на
T
так ні
const T* const
const
-вказівник на
const T
ні ні

4. Як читати оголошення без паніки

Щоб перестати сприймати const як хаос, корисно виробити просту механічну звичку читання: спочатку шукаємо імʼя змінної, а потім дивимося на символи навколо нього.

Правило для новачка (дуже практичне):

  • const ліворуч від базового типу робить константним обʼєкт, тобто те, що «за адресою».
  • const праворуч від * робить константним сам вказівник, тобто змінну-адресу.

Наприклад, в оголошенні:

const int* p;

const стосується int, отже p вказує на const int (не можна змінювати *p).

А тут:

int* const p;

const стоїть впритул до імені p, отже сам p перепризначати не можна.

Так, ви можете натрапити на «читання справа наліво» в поясненнях для досвідченіших розробників, але на вашому етапі достатньо іншої звички: відповідайте на два запитання про *p і p — і ви все зрозумієте.

5. «Звуження прав»: чому T* можна перетворити на const T*, а назад — не можна

У C++ дозволено неявно перетворити «звичайний вказівник» на «вказівник на const»:

  • було: int*
  • стало: const int*

Чому? Тому що це безпечно: ви звузили права. Було можна змінювати, стало не можна.

А от зворотне перетворення, тобто з const int* у int*, компілятор зазвичай забороняє: це вже було б «розширенням прав». Адже якщо у вас є const int* p, ви не повинні раптом отримати можливість зробити *p = 123.

#include <iostream>

int main() {
    int x = 10;

    int* p = &x;
    const int* cp = p;   // ok: звужуємо права (тепер лише читання)

    std::cout << *cp << '\n'; // 10

    // int* p2 = cp;      // не можна: розширюємо права назад
}

Це одна з причин, чому const-коректність справді допомагає: ви можете передати вказівник у функцію, яка лише читає, і компілятор гарантує, що вона не «запише» туди щось зайве.

6. Приклад: завдання й контракти функцій

Щоб усе це не лишилося теорією, продовжимо приклад із навчальним консольним застосунком: уявімо, що в нас є список завдань і ми хочемо інколи друкувати завдання, а інколи позначати його як виконане.

Модель даних — максимально проста:

#include <string>

struct Task {
    int id = 0;
    std::string title;
    bool done = false;
};

Функція, що читає, і функція, що змінює

Зробімо дві функції: перша друкує завдання й обіцяє не змінювати його. Друга — змінює завдання (ставить done = true). І саме тут const у вказівниках починає «говорити» за вас.

#include <iostream>
#include <string>

struct Task {
    int id = 0;
    std::string title;
    bool done = false;
};

void PrintTask(const Task* t) {
    if (t == nullptr) return;
    std::cout << t->id << ": " << t->title << '\n';
}

void MarkDone(Task* t) {
    if (t == nullptr) return;
    t->done = true;
}

Контракт читається прямо із сигнатури:

  • PrintTask(const Task* t) каже: «я можу отримати nullptr і не буду змінювати завдання».
  • MarkDone(Task* t) каже: «я можу отримати nullptr, але якщо завдання є, я його змінюватиму».

І невеликий main, щоб побачити, що все узгоджується:

#include <iostream>
#include <string>

struct Task {
    int id = 0;
    std::string title;
    bool done = false;
};

void PrintTask(const Task* t) {
    if (!t) return;
    std::cout << t->id << ": " << t->title << " done=" << t->done << '\n';
}

void MarkDone(Task* t) {
    if (!t) return;
    t->done = true;
}

int main() {
    Task t{1, "Read about const pointers", false};

    PrintTask(&t);  // 1: Read about const pointers done=0
    MarkDone(&t);
    PrintTask(&t);  // 1: Read about const pointers done=1
}

Тут важливо, що &t типу Task* спокійно передається в PrintTask(const Task*), тому що це безпечне «звуження прав»: функція обіцяє не змінювати обʼєкт.

Де знадобиться T* const у реальному житті

const T* зазвичай сприймається одразу: його зручно використовувати в інтерфейсі, і він добре виражає контракт «лише читання».

А T* const частіше корисний усередині реалізації. Уявіть, що всередині функції ви вибрали «поточне завдання» і далі кілька разів із ним працюєте. Важливо випадково не перепризначити вказівник на інше завдання. Тоді ви можете зафіксувати адресу:

#include <iostream>
#include <string>

struct Task {
    int id = 0;
    std::string title;
    bool done = false;
};

int main() {
    Task t{1, "Do not reassign pointer", false};

    Task* const current = &t; // адреса зафіксована
    current->done = true;     // обʼєкт можна змінювати
    // current = nullptr;      // не можна

    std::cout << current->done << '\n'; // 1
}

Тобто T* const — це насамперед захист від випадкових перепризначень, а не обовʼязковий елемент публічного інтерфейсу.

7. Типові помилки

Помилка № 1: переплутати const T* і T* const.
Одне const «приклеїли не туди» — і сенс став протилежним. Це виправляється не зубрінням, а звичкою ставити два запитання: «чи можу я зробити *p = ...?» і «чи можу я зробити p = ...?». Якщо одна з відповідей має бути «ні», ставимо const туди, де воно це забороняє.

Помилка № 2: думати, що const T* робить обʼєкт константним у всій програмі.
Ні, обʼєкт не стає назавжди «замороженим». Він лише стає «недоступним для запису через цей конкретний доступ». Інший код усе ще може змінювати обʼєкт, якщо має неконстантний доступ. Це нормально: const — інструмент керування правами, а не магічна заборона змін у всій програмі.

Помилка № 3: намагатися перетворити const T* у T*.
Зазвичай це завершується помилкою компіляції, і це добре: компілятор вас рятує. Якщо функція вимагає T*, вона збирається змінювати обʼєкт. Отже, або ви помилилися і функція має приймати const T*, або ви справді хочете змінювати обʼєкт, але тоді треба мати неконстантний доступ від самого початку, а не «викручувати руки» типам.

Помилка № 4: розіменувати вказівник, який може бути nullptr.
Поки в типі у вас T*, nullptr — допустиме значення, якщо ви не домовилися інакше. Отже, розіменування без перевірки — це лотерея. Навіть якщо «наче завжди приходить не nullptr», за місяць ви самі собі підготуєте неприємний сюрприз. Мінімальна дисципліна: якщо параметр або змінна може бути порожньою, перевірка має стояти поруч із використанням.

Помилка № 5: використовувати const як заміну проєктуванню.
Іноді новачок ставить const «скрізь, де страшно», сподіваючись, що це зробить код безпечним. Але const відповідає лише на запитання «чи можна змінювати»; він не розвʼязує питань «чи існує ще обʼєкт» і «чи валідна адреса». Тому const — чудовий спосіб задати контракт, але не бронежилет від усіх проблем.

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ