1. Навіщо потрібен const
Якщо ви лише починаєте програмувати, const може здаватися зайвою суворістю: «я й так обіцяю, що не змінюватиму значення — навіщо ще втручання компілятора?». Але в програмуванні обіцянки без перевірки працюють приблизно так само, як «я зараз швидко, на хвилинку» в магазині: інколи справді на хвилинку, а інколи ви знаходите там сенс життя й знижки. const — це спосіб перетворити обіцянку на правило, яке компілятор справді перевіряє.
Ідея проста: якщо змінна за змістом не повинна змінюватися, варто прямо сказати про це мові. Тоді компілятор стане вашим союзником: він виявлятиме випадкові зміни, а код буде зрозумілішим навіть вам самим через тиждень потому (або викладачеві за 30 секунд).
Що означає const
Почнімо з формулювання без магії. Ключове слово const означає: значення не можна змінювати після ініціалізації. Не «ніби не можна», не «не рекомендується», а саме «компілятор заборонить».
Тут важливо вловити тонкість: const означає не «значення відоме наперед», а «значення незмінне після того, як ми його отримали». Тому const чудово працює зі значеннями, які вводить користувач: ви можете зчитати число зі std::cin, а потім «заморозити» його в const-змінній, щоб випадково не зіпсувати його в подальших обчисленнях.
Невеликий приклад — найчесніша демонстрація:
#include <iostream>
int main() {
const int max_retries = 3;
std::cout << max_retries << '\n'; // 3
max_retries = 4; // помилка компіляції
return 0;
}
Тут max_retries — це «налаштування». І якщо це справді налаштування, то цілком логічно, що воно не повинно раптово змінюватися посеред виконання програми.
const вимагає ініціалізації
Зараз буде момент, коли багато хто вперше чує від компілятора тверде «ні». Звичайну змінну можна оголосити, а потім присвоїти їй значення пізніше, хоча й тут варто бути обережними. Але const-змінна зобовʼязана отримати значення одразу, інакше… що саме ми збираємося «заморожувати»?
Тому такий запис некоректний:
#include <iostream>
int main() {
const int x; // помилка: const без ініціалізації
return 0;
}
А такий — коректний:
#include <iostream>
int main() {
const int x{10};
std::cout << x << '\n'; // 10
return 0;
}
Психологічно це зручно: якщо ви бачите const, то одразу розумієте, що змінна має нормальне початкове значення. І компіляторові теж не доводиться вгадувати, що там усередині.
2. const і ініціалізація: =, (), {}
Після попередньої лекції виникає природне запитання: «а const працює лише з якимось одним видом ініціалізації?». Ні. const — це не окрема «форма ініціалізації», а модифікатор, який каже: «після ініціалізації не чіпати».
Погляньмо на три стилі в одному прикладі:
#include <iostream>
int main() {
const int a = 10;
const int b(10);
const int c{10};
std::cout << a << ' ' << b << ' ' << c << '\n'; // 10 10 10
return 0;
}
Чому часто рекомендують {}? З тієї самої причини, що й без const: фігурні дужки зазвичай краще виявляють небезпечні місця, де можна втратити дані під час перетворення типів. А ще {} візуально нагадують: «ми створюємо значення», а не «виконуємо дію».
3. const і значення на етапі компіляції
Чи можна уявити ще «константніше» значення, ніж const? Так, у C++ є окрема, суворіша ідея — константні вирази (constant expressions): те, що компілятор справді зобовʼязаний уміти обчислити наперед. Це значення або вираз, який не просто незмінний, а вже відомий до запуску програми, тобто на етапі компіляції.
Сьогодні ми не заглиблюємося в constexpr (це буде в наступній лекції), але різницю зафіксуймо:
- const відповідає на запитання: «чи можна змінювати значення після отримання?»
- constexpr відповідає на запитання: «чи має значення бути відомим під час компіляції?»
Ось приклад, який добре знімає магію: користувач вводить число, а ми робимо його const. Компілятор не міг знати це число наперед, але після введення ми забороняємо його змінювати.
#include <iostream>
int main() {
int x{};
std::cin >> x;
const int snapshot{x}; // «знімок» значення
std::cout << snapshot << '\n';
return 0;
}
4. Практика: де const особливо корисний
Сценарій «знімок» значення
Ось дуже практична думка. Іноді змінна за своєю природою є змінюваною: наприклад, ви рахуєте суму, і вона зростає. Але вам потрібно «зафіксувати» певний стан і далі використовувати його як факт. Це як зробити фото лічильника електроенергії: лічильник крутиться, але фото вже не зміниться, і саме за ним ви потім рахуєте платіж.
Сценарій простий: користувач увів базову ціну, а ви далі багато разів використовуєте її в розрахунках. Якщо залишити це значення у звичайній змінній, його можна випадково перезаписати, особливо коли код розростається. Тому й робимо «знімок».
#include <iostream>
int main() {
double entered_price{};
std::cin >> entered_price;
const double base_price{entered_price}; // фіксуємо
std::cout << base_price << '\n';
return 0;
}
Це не «обовʼязковий стиль», а інструмент. Але коли ви починаєте писати програми довші ніж на 30 рядків, то раптом розумієте: «випадково перезаписав» — це не гіпотеза, а спосіб життя.
const проти «магічних чисел»
Одна з найчастіших проблем у коді новачків — числа, які зʼявляються «просто тому, що». Сьогодні 0.2 — це ПДВ, завтра — знижка, післязавтра — коефіцієнт, сенсу якого ви вже не памʼятаєте. Такі числа називають «магічними».
const дає змогу дати числу імʼя, і це різко підвищує читабельність. Причому не треба одразу переходити до складних архітектурних ідей: достатньо просто винести коефіцієнти в окремі змінні.
Порівняйте:
#include <iostream>
int main() {
double price{100.0};
double total = price + price * 0.2; // що таке 0.2?
std::cout << total << '\n'; // 120
return 0;
}
і:
#include <iostream>
int main() {
const double vat_rate{0.2};
double price{100.0};
double total = price + price * vat_rate;
std::cout << total << '\n'; // 120
return 0;
}
У другому варіанті навіть без коментарів видно, що відбувається. І якщо ви захочете змінити ставку, то зробите це в одному місці.
Мінісхема: відокремлюємо факти від робочих змінних
Коли ви пишете програму, у ній зазвичай є змінні двох типів за змістом: «дані, які мають змінюватися» і «дані, які мають залишатися фактами». const допомагає розділити їх прямо в коді. Це спрощує налагодження: ви знаєте, які значення точно не могли зіпсувати випадковим присвоєнням.
Намалюймо просту блок-схему логіки обчислення ціни, яку зараз зберемо в єдиний приклад:
flowchart TD
A[Введення вхідних даних] --> B[Фіксуємо факти через const]
B --> C[Обчислюємо проміжні значення]
C --> D[Обчислюємо підсумок]
D --> E[Виводимо результат]
У реальному коді «фактами» часто стають коефіцієнти, ліміти, початкові параметри розрахунку, а «змінюваними» — накопичувачі, суми, лічильники й поточні значення.
5. Практичний приклад: мінікаса
Тепер зберімо невелику консольну програму, яку подумки можна вважати нашим «навчальним застосунком» у межах поточних тем. Ми не використовуємо функції, контейнери й усе таке — це буде пізніше. Зате чесно застосовуємо змінні, ініціалізацію та const.
Сюжет простий: користувач вводить ціну товару й відсоток знижки. Ставка ПДВ у нас фіксована. Ми обчислюємо підсумкову ціну.
Версія 1: наївно (і крихко)
Почнімо з простого варіанту. Технічно він працює, але його легко зламати випадковими правками:
#include <iostream>
int main() {
double price{};
double discount_percent{};
std::cin >> price >> discount_percent;
discount_percent = discount_percent / 100.0; // повторно використовуємо змінну
double total = price - price * discount_percent;
std::cout << total << '\n';
return 0;
}
Проблема не в тому, що це «погано». Проблема в тому, що змінна discount_percent змінила свій зміст: спочатку це «відсотки», потім — «частка». За кілька днів ви вже забудете, на якому етапі перебуваєте.
Версія 2: відокремлюємо факти й фіксуємо
Зробімо акуратніше: введені значення зафіксуємо в const, а проміжні обчислення винесемо окремо.
#include <iostream>
int main() {
double price_input{};
double discount_input{};
std::cin >> price_input >> discount_input;
const double price{price_input};
const double discount_percent{discount_input};
const double discount_rate{discount_percent / 100.0};
double total{price - price * discount_rate};
std::cout << total << '\n';
return 0;
}
Тут добре видно, що discount_percent не змінюється, discount_rate — це окрема сутність, і ви не перетворюєте одну змінну на «хамелеона».
Версія 3: додаємо ПДВ як const
Тепер ускладнімо приклад буквально однією ідеєю: ставка ПДВ фіксована й не повинна змінюватися.
#include <iostream>
int main() {
const double vat_rate{0.2};
double price_input{};
double discount_input{};
std::cin >> price_input >> discount_input;
const double price{price_input};
const double discount_rate{discount_input / 100.0};
const double discounted{price - price * discount_rate};
const double total{discounted + discounted * vat_rate};
std::cout << total << '\n';
return 0;
}
Зверніть увагу на ефект: майже всі змінні стали const, бо ми їх не зобовʼязані змінювати. Змінюваними залишаються лише «вхідні буфери» (price_input, discount_input) — і то, власне кажучи, ми могли б не робити їх змінюваними, просто так наочніше. У результаті програма стає схожою на послідовність формул, а для розрахункових задач це дуже приємний стиль.
6. const і підтримка коду в майбутньому
Є простий критерій: якщо змінна змінюється, читачеві доводиться шукати в коді, де саме і чому це сталося. Якщо змінна const, читач розслабляється: «гаразд, це факт, його можна використовувати, але не треба відстежувати, де він міг змінитися».
Це особливо корисно, коли ви виправляєте помилки. Часто проблема виглядає так: «чому сума стала відʼємною?». А потім виявляється, що десь випадково перезаписали коефіцієнт або «початкову суму». const перетворює такі проблеми на помилки компіляції — тобто ви ловите їх раніше, ніж програма взагалі запуститься.
Є й приємний бонус: const дисциплінує. Він змушує вас подумати: «це параметр?», «це налаштування?», «це факт?» чи «це робоча змінна?». І поступово код стає менш випадковим.
7. Типові помилки під час роботи з const
Помилка № 1: оголосити const без ініціалізації.
Так не вийде, бо const має «заморозити» конкретне значення. Якщо написати const int x;, то незрозуміло, що саме ми забороняємо змінювати. Правильний шлях — ініціалізувати все одразу: const int x{10}; або const int x = 10;.
Помилка № 2: ставити const на змінну, яка за змістом має змінюватися.
Іноді новачки намагаються «зробити все безпечним» і пишуть const навіть для суми, яку накопичують, або для лічильника. Компілятор почне сваритися, а ви — дратуватися. Тут важливо розрізняти: const — для фактів (ліміт, ставка, початкові дані, знімок значення), а змінювані змінні залишаємо звичайними.
Помилка № 3: плутати const і «значення відоме компілятору».
const — це заборона змінювати значення після ініціалізації. Значення може надійти від користувача, з введення, з етапу виконання — і все одно бути const. А вимога «значення обчислюється під час компіляції» стосується іншої сутності, про яку ми говоритимемо в наступній лекції (constexpr).
Помилка № 4: змінювати зміст змінної «по дорозі» замість того, щоб створити нову.
Класичний приклад: змінна була «відсотки», стала «частка», а потім — «підсумкова знижка в євро». Це робить код крихким. const часто допомагає впіймати такий стиль мислення й змушує заводити нові зрозумілі імена: discount_percent, discount_rate, discount_value.
Помилка № 5: боятися const, бо «раптом доведеться змінити».
Це дуже людська звичка — залишати собі «запасний вихід». Але в коді такий запасний вихід перетворюється на діру: завтра ви або хтось інший справді випадково зміните те, що змінювати не можна. Якщо за змістом значення не повинно змінюватися — зафіксуйте це. Якщо пізніше зʼясується, що все-таки повинно, — ви знімете const усвідомлено, у конкретному місці й з розумінням причини.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ