JavaRush /Курси /C++ SELF /static-члени класу: спільні налаштування та генерація id

static-члени класу: спільні налаштування та генерація id

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

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-лічильник демонстрацією механіки або метрикою «скільки разів створювали», але не намагатися робити з нього «точну кількість живих екземплярів».

1
Задача
C++ SELF, 49 рівень, 1 лекція
Недоступна
Загальна довжина
Загальна довжина
1
Задача
C++ SELF, 49 рівень, 1 лекція
Недоступна
Автоідентифікатор
Автоідентифікатор
1
Задача
C++ SELF, 49 рівень, 1 лекція
Недоступна
Курс покупок
Курс покупок
1
Задача
C++ SELF, 49 рівень, 1 лекція
Недоступна
Статистика гравців
Статистика гравців
Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ