1. Що таке static-члени класу
Коли ви починаєте писати класи, дуже легко впасти в крайність «усе — це поля обʼєкта». І в якийсь момент ви помічаєте дивну річ: у вас є 10 обʼєктів, і кожен із них зберігає одне й те саме значення «за замовчуванням», те саме налаштування, той самий довідник. Це якби в кожному підʼїзді діяв свій окремий закон фізики: наче й можна, але зазвичай це зайве.
static-члени дають змогу описати дані та функції, які належать класу як ідеї, а не конкретному екземпляру. У реальному коді це часто виглядає як «спільна конфігурація», «лічильник виданих id», «таблиця перекладу статусів у текст», «спільний ліміт». Це також один зі способів акуратно уникнути глобальних змінних, не перетворюючи проєкт на «усе доступне звідусіль».
Нестатичні поля та static-поля: хто чим володіє
Якщо казати просто, нестатичне поле — це частина кожного обʼєкта. А static-поле — це частина класу, і воно існує в єдиному екземплярі на всю програму, точніше — на весь «світ», у якому живе цей клас.
Уявіть, що в нас є клас Task у міні-застосунку «список задач». У кожної задачі є заголовок і прапорець «виконано». Це дані конкретної задачі. А от «наступний id» — це вже не про конкретну задачу, а про всю систему загалом.
Ось невеличка схема для наочності (дуже умовна, тож не судіть суворо):
flowchart LR
subgraph Program["Програма"]
subgraph ClassTask["Клас Task"]
NextId["static next_id_ (один на всіх)"]
end
T1["task1: title_, done_, id_"]
T2["task2: title_, done_, id_"]
T3["task3: title_, done_, id_"]
NextId --> T1
NextId --> T2
NextId --> T3
end
А ось коротка таблиця для закріплення:
| Властивість | Нестатичне поле (title_, done_) | static-поле (next_id_) |
|---|---|---|
| Скільки екземплярів | По одному на обʼєкт | Один на весь клас |
| Чи можна звертатися без обʼєкта | Ні | Так |
| Типовий сенс | Стан конкретної сутності | Спільні налаштування / спільний ресурс / «глобалка для класу» |
2. static-методи: функції без конкретного обʼєкта
Із даними розібралися, тепер — про поведінку. static-метод — це метод, який можна викликати без обʼєкта. У нього немає «прихованого параметра this», а отже, він не знає, до якого саме обʼєкта звертатися.
І це логічно: якщо метод не про конкретну задачу, а про «налаштування застосунку» чи «генерацію id», то викликати його на обʼєкті не обовʼязково.
Міні-приклад: зробімо клас-конфігурацію для нашого застосунку задач.
#include <iostream>
class AppConfig {
static bool show_done_;
public:
static void set_show_done(bool value) { show_done_ = value; }
static bool show_done() { return show_done_; }
};
bool AppConfig::show_done_ = true;
int main() {
std::cout << AppConfig::show_done() << '\n'; // 1
AppConfig::set_show_done(false);
std::cout << AppConfig::show_done() << '\n'; // 0
}
Тут важливо відчути одну річ: ми не створювали AppConfig config;. І це нормально. У цьому випадку клас використовується як «контейнер» для спільного налаштування.
А тепер типова перевірка на розуміння: чому static-метод не може просто взяти й вивести title_ задачі? Тому що в нього немає задачі. Йому просто немає до чого привʼязатися.
3. Де живе static-поле і чому його потрібно визначити
Ось тут зазвичай і відбувається перший справжній контакт із лінкером. Не з компілятором: він може бути цілком задоволений, а от лінкер потім скаже: «Ви оголосили static-поле, але де воно насправді лежить у памʼяті?»
Для початку достатньо запамʼятати просте правило:
Якщо ви написали static-поле в класі, то ви оголосили його.
Щоб воно справді існувало, його треба визначити один раз десь у .cpp (або в нашому навчальному випадку — нижче у файлі).
Погляньмо на приклад:
#include <iostream>
class Counter {
static int total_;
public:
static void inc() { ++total_; }
static int total() { return total_; }
};
// Визначення static-поля (без цього буде помилка лінкування)
int Counter::total_ = 0;
int main() {
Counter::inc();
std::cout << Counter::total() << '\n'; // 1
}
Якщо забути рядок int Counter::total_ = 0;, компілятор часто цього не помітить. Але на етапі лінкування ви отримаєте помилку на кшталт «undefined reference» / «unresolved external symbol» (формулювання залежить від компілятора та IDE). Це повідомлення означає: «Я знаю, що така змінна має бути, але не можу її знайти».
Ремарка про сучасний C++
Іноді ви можете натрапити в коді на таке:
inline static int total_ = 0;
Це означає, що оголошення водночас є і визначенням (у сучасних стандартах).
Але на нашому рівні корисніше спочатку вивчити базовий, максимально зрозумілий шлях: «оголосили в класі — визначили окремо». Він працює всюди й допомагає краще відчути, що саме відбувається.
4. Сценарій: спільні налаштування застосунку
Один із найприродніших сценаріїв для static — спільне налаштування, яке впливає на поведінку всієї програми. Наприклад, чи показувати виконані задачі, або яку максимальну довжину заголовка ми вважаємо нормальною, щоб не перетворювати консоль на суцільне простирадло тексту.
У нашому навчальному міні-застосунку «TaskList» (умовному списку задач) заведімо два налаштування:
- показувати виконані задачі або приховувати їх;
- максимальна довжина заголовка під час друку (якщо довше — обріжемо).
Зробімо це через static в окремому класі.
#include <string>
class AppConfig {
static bool show_done_;
static std::size_t max_title_len_;
public:
static bool show_done() { return show_done_; }
static void set_show_done(bool v) { show_done_ = v; }
static std::size_t max_title_len() { return max_title_len_; }
static void set_max_title_len(std::size_t v) { max_title_len_ = v; }
};
bool AppConfig::show_done_ = true;
std::size_t AppConfig::max_title_len_ = 20;
Тут ми використали std::size_t (ми з ним уже знайомі за контейнерами та size()), і це добрий тип для значень довжини.
Зверніть увагу на стиль: доступ до даних відбувається через методи. Можна було б зробити public static-поле, але це швидко перетворюється на ситуацію «хто завгодно змінює що завгодно». А потім ви шукаєте, хто поставив max_title_len_ = 999999;.
5. Сценарій: генерація id для обʼєктів
Ще один класичний сценарій для static: потрібно, щоб кожен новий обʼєкт отримував унікальний ідентифікатор. Причому унікальний не «всередині обʼєкта», а «в усій програмі».
Зробімо клас Task, де:
- id_ — особистий номер конкретної задачі (звичайне поле);
- next_id_ — спільний лічильник «який id видавати наступним» (static-поле).
Каркас класу
#include <string>
class Task {
int id_;
std::string title_;
bool done_ = false;
static int next_id_;
public:
explicit Task(std::string title);
int id() const { return id_; }
const std::string& title() const { return title_; }
};
Поки що ми лише оголосили static int next_id_;. Тепер важливий момент: далі буде і визначення.
Конструктор і визначення next_id_
#include <utility>
int Task::next_id_ = 1;
Task::Task(std::string title)
: id_(next_id_++)
, title_(std::move(title))
{}
Тут добре видно саму ідею: під час створення обʼєкта ми беремо поточне значення next_id_, записуємо його в id_, а потім збільшуємо next_id_.
Так, next_id_++ — це «спочатку використай поточне значення, а потім збільш». На цьому місці зазвичай хтось каже: «Я зрозумів, але звучить як закляття». Це нормально — просто зафіксуйте сенс.
Перевіримо в main
#include <iostream>
int main() {
Task a{"Read C++ book"};
Task b{"Write C++ code"};
std::cout << a.id() << '\n'; // 1
std::cout << b.id() << '\n'; // 2
}
Важливо: ми ніде вручну не задавали id. Він «зʼявився сам» під час створення обʼєктів. Це як номерок у гардеробі: ви не сперечаєтеся з гардеробником, а просто приймаєте правила гри.
6. static-лічильники: демонстрація та обмеження
Дуже спокуслива думка: «А давайте порахуємо, скільки в нас є обʼєктів Task». І рука сама тягнеться до static int count_.
На цьому етапі важливо розуміти: лічильник створених обʼєктів і кількість живих обʼєктів — це різні речі. До того ж є копіювання обʼєктів (яке теж створює новий обʼєкт), є тимчасові обʼєкти, і в якийсь момент ви раптом розумієте, що «лічильник живих» — це вже не іграшка, а маленька бухгалтерія.
Але як демонстрація можливостей static такий приклад цілком підходить: «скільки разів ми створювали задачі».
#include <iostream>
class TaskFactoryStats {
static int created_;
public:
static void on_created() { ++created_; }
static int created() { return created_; }
};
int TaskFactoryStats::created_ = 0;
int main() {
TaskFactoryStats::on_created();
TaskFactoryStats::on_created();
std::cout << TaskFactoryStats::created() << '\n'; // 2
}
Тут ми чесно називаємо речі своїми іменами: це не «скільки обʼєктів зараз існує», а «скільки разів ми зафіксували створення». Це корисно для статистики або логів, але не варто робити з цього «істину всесвіту».
7. Приклад: друк задач з урахуванням static-налаштувань
Тепер подивімося, як налаштування справді впливають на поведінку програми. Напишемо дуже маленьку функцію для друку заголовка задачі, яка обрізає рядок відповідно до AppConfig::max_title_len().
Зробімо це окремою функцією (поки без ускладнень — просто щоб відчути, як одне налаштування впливає на всі місця, де його використовують).
#include <iostream>
#include <string>
std::string shorten(std::string s, std::size_t max_len) {
if (s.size() <= max_len) return s;
return s.substr(0, max_len) + "...";
}
int main() {
std::cout << shorten("Very very long task title", 10) << '\n'; // Very very ...
}
Тепер зробімо ще один крок: max_len можна брати з AppConfig. І от тут static-поле починає відчуватися як справжній інструмент: ви змінюєте налаштування один раз — і всі місця, які його читають, починають поводитися інакше.
8. Типові помилки під час роботи зі static-членами
Помилка № 1: забули визначити static-поле (і спіймали помилку лінкування).
Це найчастіша ситуація: усередині класу static int next_id_; написали, а рядка int Task::next_id_ = 1; — немає. У результаті код може навіть компілюватися, але на етапі збирання ви отримаєте «undefined reference / unresolved external symbol». Виправлення просте: у кожного static-поля має бути рівно одне визначення.
Помилка № 2: намагаються звернутися до нестатичного поля зі static-методу «якось».
Іноді хочеться написати в static-методі std::cout << title_; Але title_ — це поле обʼєкта, а static-метод не знає, якого саме обʼєкта. Якщо потрібно обробити конкретну задачу, передавайте обʼєкт параметром або робіть звичайний метод, який викликається на обʼєкті.
Помилка № 3: роблять static там, де має бути індивідуальний стан.
Наприклад, «статус задачі» або «заголовок» за змістом належать конкретній задачі, а не всім задачам одразу. Якщо зробити їх static, тоді раптом усі задачі матимуть один спільний заголовок. І це буде дуже дивний список задач, де все називається однаково — як теки «Нова тека (37)» на робочому столі.
Помилка № 4: використовують static як «зручну глобальну змінну» без дисципліни.
static-налаштування легко перетворити на приховану глобальщину: хтось в одному місці коду змінив налаштування, а в іншому все почало поводитися інакше, і ви довго шукаєте причину. Тому добрий стиль — ховати static-дані за методами get/set, давати їм зрозумілі імена й використовувати тільки там, де справді потрібно щось «спільне для всіх».
Помилка № 5: сприймають лічильник на static як точний облік обʼєктів.
Навіть якщо сьогодні ви додаєте лічильник у конструкторі, завтра хтось почне копіювати обʼєкти (іноді навіть «випадково»), і статистика стане несподіваною. На нашому рівні найкращий підхід — вважати static-лічильник демонстрацією механіки або метрикою «скільки разів створювали», але не намагатися робити з нього «точну кількість живих екземплярів».
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ