C++

Статичні члени класу

Статичні поля та методи класу, різниця між даними об'єкта та даними класу, ініціалізація статичних полів поза класом, inline static (C++17), практичні патерни: лічильник об'єктів, генератор ID, застереження щодо чисто статичних класів.

Статичні члени класу

Дані об'єкта проти даних класу

До цього моменту всі поля класів, які ми оголошували, належали конкретним об'єктам. Коли ви створюєте об'єкт Date today(16, 9, 2026);, цей об'єкт отримує власні копії полів day, month, year. Якщо ви створюєте інший об'єкт Date birthday(5, 12, 2005);, він отримує окремий набір тих самих полів із власними значеннями. Кожен об'єкт живе у своїй ізольованій пам'яті.

Але що, якщо потрібно зберігати дані, спільні для всіх об'єктів класу? Наприклад:

  • Лічильник об'єктів: скільки екземплярів класу Student було створено?
  • Генератор унікальних ID: кожен новий об'єкт отримує наступний порядковий номер
  • Константа класу: конфігураційне значення, однакове для всіх об'єктів (наприклад, MAX_NAME_LENGTH)
  • Спільний ресурс: підключення до бази даних, таблиця пошуку, кеш

Для таких даних створювати окрему копію в кожному об'єкті — марнотратство пам'яті та джерело помилок синхронізації. Рішення — статичні члени класу.

Статичний член класу (static member) — це поле або метод, що належить класу загалом, а не окремим об'єктам. Він існує в єдиному екземплярі, незалежно від кількості створених об'єктів — навіть якщо об'єктів немає взагалі.

Статичні члени класу — це не глобальні змінні у повному розумінні. Вони інкапсульовані всередині класу, підлягають специфікаторам доступу (private, public) і логічно пов'язані з класом. Це організований спосіб зберігання даних, спільних для всіх об'єктів.

Статичні поля: синтаксис та семантика

Статичне поле оголошується додаванням ключового слова static перед типом поля:

class ClassName
{
private:
    static int sharedValue;  // Статичне поле
};

Приклад: звичайне поле проти статичного

Counter.cpp
#include <iostream>

class Counter
{
public:
    int normalValue;         // Звичайне поле — окреме для кожного об'єкта
    static int staticValue;  // Статичне поле — спільне для всіх об'єктів
};

// Визначення статичного поля поза класом (обов'язково!)
int Counter::staticValue = 0;

int main()
{
    Counter c1, c2;

    c1.normalValue = 10;
    c2.normalValue = 20;

    std::cout << "c1.normalValue: " << c1.normalValue << "\n";
    std::cout << "c2.normalValue: " << c2.normalValue << "\n";

    c1.staticValue = 100;
    std::cout << "c1.staticValue: " << c1.staticValue << "\n";
    std::cout << "c2.staticValue: " << c2.staticValue << "\n";  // Те саме значення!

    c2.staticValue = 200;
    std::cout << "c1.staticValue: " << c1.staticValue << "\n";  // Змінилося і тут!

    return 0;
}
./program
$ ./program
c1.normalValue: 10
c2.normalValue: 20
c1.staticValue: 100
c2.staticValue: 100
c1.staticValue: 200
Execution finished with exit code 0.

Що показує приклад

Звичайне поле normalValue:

  • c1.normalValue = 10 впливає лише на c1
  • c2.normalValue = 20 впливає лише на c2
  • Два об'єкти — дві незалежні змінні

Статичне поле staticValue:

  • c1.staticValue = 100 змінює єдину змінну Counter::staticValue
  • c2.staticValue читає ту саму змінну — значення 100
  • c2.staticValue = 200 змінює ту саму змінну
  • c1.staticValue тепер також показує 200

Обидва c1.staticValue і c2.staticValue — це різні імена для однієї й тієї ж змінної.


Доступ до статичних полів: через клас, а не через об'єкт

Хоча до статичного поля можна звертатися через об'єкт (c1.staticValue), правильніше та явніше звертатися через ім'я класу:

Counter::staticValue = 100;
std::cout << Counter::staticValue << "\n";

Це підкреслює, що поле належить класу, а не конкретному об'єкту.

AccessStatic.cpp
#include <iostream>

class Example
{
public:
    static int sharedData;
};

int Example::sharedData = 42;

int main()
{
    // ✅ Рекомендований спосіб — через ім'я класу
    std::cout << Example::sharedData << "\n";
    Example::sharedData = 100;

    // ⚠️ Допустимо, але менш явно
    Example obj;
    std::cout << obj.sharedData << "\n";
    obj.sharedData = 200;

    // Обидва способи змінюють ту саму змінну
    std::cout << Example::sharedData << "\n";  // 200

    return 0;
}
Правило стилю: завжди звертайтеся до статичних членів через ім'я класу (ClassName::member), а не через об'єкт (object.member). Це робить код зрозумілішим — відразу видно, що член статичний.

Оголошення vs визначення: обов'язкова ініціалізація поза класом

Найважливіша деталь синтаксису статичних полів: оголошення всередині класу — це лише оголошення, а не визначення. Статичне поле має бути визначене і ініціалізоване поза тілом класу — зазвичай у файлі .cpp.

Чому так?

Статичні поля не є частиною об'єктів — вони не зберігаються в пам'яті кожного об'єкта. Вони схожі на глобальні змінні (але інкапсульовані в клас) і ініціалізуються один раз при запуску програми. Визначення має бути в одиниці трансляції (.cpp файлі), а не в заголовку, щоб уникнути порушення ODR (One Definition Rule).

Синтаксис визначення

ТипДаних ClassName::staticField = значення;

Важливо:

  • Без ключового слова static у визначенні! Воно потрібне лише в оголошенні.
  • Оператор ClassName:: обов'язковий — він вказує, що змінна належить класу.

Приклад: заголовок і реалізація

Student.h
#ifndef STUDENT_H
#define STUDENT_H

#include <string>

class Student
{
private:
    std::string name;
    int age;

    static int studentCount;  // Оголошення — лише повідомлення компілятору

public:
    Student(const std::string& n, int a);
    ~Student();

    static int getStudentCount();  // Статичний метод (розглянемо нижче)
};

#endif
Student.cpp
#include "Student.h"

// Визначення статичного поля — обов'язково поза класом!
int Student::studentCount = 0;

Student::Student(const std::string& n, int a) : name(n), age(a)
{
    studentCount++;  // Збільшуємо лічильник при створенні об'єкта
}

Student::~Student()
{
    studentCount--;  // Зменшуємо лічильник при знищенні об'єкта
}

int Student::getStudentCount()
{
    return studentCount;
}
main.cpp
#include <iostream>
#include "Student.h"

int main()
{
    std::cout << "Початкова кількість студентів: " << Student::getStudentCount() << "\n";

    {
        Student s1("Олена", 20);
        Student s2("Іван", 22);

        std::cout << "Створено двох студентів: " << Student::getStudentCount() << "\n";

        Student s3("Марія", 19);
        std::cout << "Створено третього: " << Student::getStudentCount() << "\n";
    }  // s1, s2, s3 виходять зі scope — викликаються деструктори

    std::cout << "Після виходу зі scope: " << Student::getStudentCount() << "\n";

    return 0;
}
./program
$ ./program
Початкова кількість студентів: 0
Створено двох студентів: 2
Створено третього: 3
Після виходу зі scope: 0
Execution finished with exit code 0.

Аналіз прикладу

Рядок 4 у Student.cpp:

int Student::studentCount = 0;

Це визначення статичного поля. Без цього рядка програма не скомпілюється (помилка лінкера «undefined reference»).

Конструктор:

studentCount++;

Кожен новий об'єкт збільшує спільний лічильник.

Деструктор:

studentCount--;

Кожен знищений об'єкт зменшує лічильник.

Виклик у main:

Student::getStudentCount()

Доступ до лічильника через статичний метод (детально нижче).

Найпоширеніша помилка: забути визначити статичне поле поза класом. Компілятор успішно обробить заголовок, але лінкер видасть помилку: undefined reference to 'Student::studentCount'.

Статичні константи: виняток з правила

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

class Config
{
public:
    static const int MAX_USERS = 100;  // ✅ Дозволено для const int
    static const double PI = 3.14159;  // ❌ Помилка — double не цілочисельний
};

У цьому випадку не потрібно окреме визначення поза класом (хоча воно не буде помилкою).

C++17: inline static

Починаючи з C++17, ключове слово inline дозволяє ініціалізувати будь-яке статичне поле відразу в класі:

class Config
{
public:
    inline static int maxConnections = 50;      // ✅ C++17
    inline static double timeout = 30.5;        // ✅ C++17
    inline static std::string serverName = "localhost";  // ✅ C++17
};

З inline static не потрібно окреме визначення у .cpp — все відбувається в заголовку.

inline для статичних полів працює так само, як для inline-функцій: множинні визначення (у кількох одиницях трансляції) дозволені, компілятор об'єднує їх в одне.

Статичні методи: функції без прив'язки до об'єкта

Статичний метод — це метод класу, який не має доступу до конкретного об'єкта. Він працює лише зі статичними полями та параметрами.

Синтаксис

class ClassName
{
public:
    static returnType methodName(parameters);
};

Ключові відмінності від звичайних методів

❌ Немає this

Статичний метод не має неявного покажчика this, адже він не прив'язаний до конкретного об'єкта. Це логічно: який об'єкт мав би вказувати this, якщо метод викликається як ClassName::method()?

🚫 Не може звертатися до нестатичних членів

Статичний метод не може читати чи змінювати звичайні поля об'єкта (name, age тощо), адже йому невідомо, про який об'єкт йдеться. Доступ лише до статичних полів.

✅ Викликається через клас

Статичні методи викликаються через ім'я класу: ClassName::method(). Виклик через об'єкт (object.method()) допустимий, але не рекомендований.

Приклад: генератор унікальних ID

IDGenerator.cpp
#include <iostream>

class IDGenerator
{
private:
    static int nextID;

public:
    static int getNextID()
    {
        return nextID++;
    }
};

int IDGenerator::nextID = 1;  // Починаємо з 1

int main()
{
    std::cout << "ID 1: " << IDGenerator::getNextID() << "\n";
    std::cout << "ID 2: " << IDGenerator::getNextID() << "\n";
    std::cout << "ID 3: " << IDGenerator::getNextID() << "\n";
    std::cout << "ID 4: " << IDGenerator::getNextID() << "\n";

    return 0;
}
./program
$ ./program
ID 1: 1
ID 2: 2
ID 3: 3
ID 4: 4
Execution finished with exit code 0.

Чому метод статичний?

  • Генератор ID не залежить від конкретного об'єкта
  • Не потрібно створювати екземпляр класу для отримання ID
  • Метод працює лише зі статичним полем nextID

Визначення статичних методів поза класом

Як і звичайні методи, статичні можна визначати поза тілом класу. Важливо: ключове слово static використовується лише в оголошенні, не в визначенні.

Math.h
#ifndef MATH_H
#define MATH_H

class Math
{
public:
    static int add(int a, int b);
    static int multiply(int a, int b);
};

#endif
Math.cpp
#include "Math.h"

// Без слова static у визначенні!
int Math::add(int a, int b)
{
    return a + b;
}

int Math::multiply(int a, int b)
{
    return a * b;
}
main.cpp
#include <iostream>
#include "Math.h"

int main()
{
    std::cout << "5 + 3 = " << Math::add(5, 3) << "\n";
    std::cout << "5 * 3 = " << Math::multiply(5, 3) << "\n";
    return 0;
}
Типова помилка: додати static у визначення методу поза класом. Це призведе до помилки — компілятор сприйме це як статичну функцію з внутрішнім зв'язуванням, а не як метод класу.

Практичний патерн: клас із унікальним ID для кожного об'єкта

Розглянемо клас Entity, де кожен об'єкт автоматично отримує унікальний ідентифікатор при створенні.

Entity.h
#ifndef ENTITY_H
#define ENTITY_H

#include <string>

class Entity
{
private:
    int id;
    std::string name;

    static int nextID;

public:
    Entity(const std::string& entityName);

    int getID() const { return id; }
    std::string getName() const { return name; }

    void print() const;

    static int getTotalEntities() { return nextID - 1; }
};

#endif
Entity.cpp
#include "Entity.h"
#include <iostream>

int Entity::nextID = 1;

Entity::Entity(const std::string& entityName) : name(entityName)
{
    id = nextID++;  // Захоплюємо поточний ID і збільшуємо для наступного
}

void Entity::print() const
{
    std::cout << "Entity #" << id << ": " << name << "\n";
}
main.cpp
#include <iostream>
#include "Entity.h"

int main()
{
    Entity player("Player");
    Entity enemy1("Goblin");
    Entity enemy2("Orc");
    Entity boss("Dragon");

    player.print();
    enemy1.print();
    enemy2.print();
    boss.print();

    std::cout << "\nВсього створено сутностей: " << Entity::getTotalEntities() << "\n";

    return 0;
}
./program
$ ./program
Entity #1: Player
Entity #2: Goblin
Entity #3: Orc
Entity #4: Dragon
Всього створено сутностей: 4
Execution finished with exit code 0.

Як це працює

Статичне поле nextID:

  • Спільне для всіх об'єктів
  • Зберігає наступний доступний ID
  • Ініціалізується значенням 1

Конструктор:

id = nextID++;
  • Присвоює поточний nextID полю id об'єкта
  • Збільшує nextID для наступного об'єкта
  • Кожен об'єкт отримує унікальний, послідовний ID

Статичний метод getTotalEntities():

return nextID - 1;
  • Повертає кількість створених об'єктів
  • nextID завжди на 1 більший за останній виданий ID

Обмеження статичних методів

Статичні методи мають жорсткі обмеження через відсутність this:

class Example
{
private:
    int value;
    static int sharedValue;

public:
    static void printValue()
    {
        std::cout << value << "\n";  // ❌ Помилка!
        // Який об'єкт? Який value?
    }

    static void setValue(int v)
    {
        value = v;  // ❌ Помилка!
    }

    static void callNonStatic()
    {
        print();  // ❌ Помилка, якщо print() нестатичний
    }
};
Правило: статичний метод може звертатися лише до:
  1. Статичних полів класу
  2. Інших статичних методів класу
  3. Своїх параметрів
  4. Локальних змінних

Візуалізація пам'яті: звичайні vs статичні поля

Щоб краще зрозуміти різницю, розглянемо структуру пам'яті:

class Player
{
private:
    std::string name;      // Нестатичне — копія в кожному об'єкті
    int health;            // Нестатичне — копія в кожному об'єкті

    static int playerCount;  // Статичне — одна копія на весь клас

public:
    Player(const std::string& n) : name(n), health(100)
    {
        playerCount++;
    }
};

int Player::playerCount = 0;

Структура пам'яті:

Player::playerCount (глобальна/статична область)
┌─────────────┐
│ playerCount │  ← Одна змінна для всього класу
│     2       │
└─────────────┘

Об'єкт p1 (стек/купа)          Об'єкт p2 (стек/купа)
┌─────────────┐                ┌─────────────┐
│ name: "Hero"│                │ name: "NPC" │
│ health: 100 │                │ health: 100 │
└─────────────┘                └─────────────┘

Кожен об'єкт має власні копії name і health. Але один playerCount існує поза об'єктами і спільний для всіх.


Чисто статичні класи: застереження

Чисто статичний клас — це клас, у якого всі члени (поля та методи) є статичними. По суті, це обгортка для глобальних функцій і змінних.

class MathUtils
{
public:
    static int add(int a, int b) { return a + b; }
    static int subtract(int a, int b) { return a - b; }
    static double sqrt(double x) { /* ... */ }
};

// Використання: MathUtils::add(5, 3);

Проблеми чисто статичних класів

⚠️ Неможливість множинних екземплярів

Статичні члени існують в одному екземплярі. Якщо вам потрібні два незалежні генератори ID або дві конфігурації — чисто статичний клас не підходить.

🌐 Фактично глобальні

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

🔒 Складність тестування

Статичний стан зберігається між тестами. Важко ізолювати тести один від одного. Потрібні явні методи скидання стану.

Коли чисто статичні класи прийнятні

  • Утилітарні функції без стану: MathUtils, StringHelper
  • Константи та конфігурація: Config::MAX_CONNECTIONS
  • Фабричні методи: Object::create()
Якщо клас логічно представляє об'єкт (студент, машина, файл), він не має бути чисто статичним. Створюйте звичайні об'єкти. Статичні члени використовуйте лише для даних, спільних для всіх об'єктів.

Статичні константи: практичний приклад

Config.h
#ifndef CONFIG_H
#define CONFIG_H

#include <string>

class ServerConfig
{
public:
    static const int DEFAULT_PORT = 8080;
    static const int MAX_CONNECTIONS = 100;
    static const int TIMEOUT_SECONDS = 30;

    inline static std::string DEFAULT_HOST = "localhost";  // C++17
    inline static bool DEBUG_MODE = false;                 // C++17
};

#endif

Використання:

#include <iostream>
#include "Config.h"

void startServer()
{
    std::cout << "Запуск сервера...\n";
    std::cout << "  Хост: " << ServerConfig::DEFAULT_HOST << "\n";
    std::cout << "  Порт: " << ServerConfig::DEFAULT_PORT << "\n";
    std::cout << "  Макс. підключень: " << ServerConfig::MAX_CONNECTIONS << "\n";
    
    if (ServerConfig::DEBUG_MODE) {
        std::cout << "  [DEBUG MODE ENABLED]\n";
    }
}

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


C++ не підтримує статичні конструктори

На відміну від деяких мов (C#, Java), C++ не має статичних конструкторів — спеціальних методів, що автоматично викликаються для ініціалізації статичних полів.

Проблема

Що, якщо статичне поле потребує складної ініціалізації — наприклад, заповнення масиву через цикл?

class LookupTable
{
public:
    static std::vector<int> precomputedValues;  // Як ініціалізувати?
};

Рішення 1: ініціалізація у визначенні (для простих випадків)

std::vector<int> LookupTable::precomputedValues = {1, 2, 4, 8, 16, 32};

Рішення 2: допоміжна функція

class LookupTable
{
private:
    static std::vector<int> initializeTable()
    {
        std::vector<int> table;
        for (int i = 0; i < 10; i++) {
            table.push_back(i * i);
        }
        return table;
    }

public:
    static std::vector<int> precomputedValues;
};

std::vector<int> LookupTable::precomputedValues = LookupTable::initializeTable();

При визначенні precomputedValues викликається initializeTable(), яка виконує всю логіку ініціалізації.

Рішення 3: статичний об'єкт-ініціалізатор (патерн для C++98/03)

class Data
{
private:
    static std::vector<int> values;

    class Initializer
    {
    public:
        Initializer()
        {
            values.push_back(10);
            values.push_back(20);
            values.push_back(30);
        }
    };

    static Initializer init;

public:
    static void printValues()
    {
        for (int v : values) {
            std::cout << v << " ";
        }
        std::cout << "\n";
    }
};

std::vector<int> Data::values;
Data::Initializer Data::init;  // Конструктор Initializer виконує ініціалізацію

При визначенні init викликається конструктор Initializer(), який заповнює values.

У сучасному C++ (C++11+) рішення 2 (допоміжна функція) є найпростішим і найчитабелішим.

Практичне завдання: клас BankAccount з генератором номерів


Підсумкова таблиця: статичні члени класу

АспектЗвичайний членСтатичний член
НалежитьКонкретному об'єктуКласу загалом
Кількість копійПо одній на кожен об'єктОдна на весь клас
СтворенняПри створенні об'єктаПри запуску програми
Доступobject.memberClassName::member (рекомендовано)
this у методахЄНемає
Доступ до інших членівДо всіхЛише до статичних
ІніціалізаціяУ конструкторіПоза класом (.cpp)
Виняток ініціалізаціїconst int у класі, inline static (C++17)

Коли використовувати статичні члени

✅ Використовуйте статичні поля

  • Лічильник об'єктів класу
  • Генератор унікальних ID
  • Конфігураційні константи
  • Спільні ресурси (підключення до БД, кеш)
  • Дані, що не залежать від конкретного об'єкта

✅ Використовуйте статичні методи

  • Фабричні методи створення об'єктів
  • Утилітарні функції без стану
  • Доступ до статичних полів
  • Операції, що не потребують об'єкта

❌ Не використовуйте статичні члени

  • Для даних, специфічних для об'єкта
  • Коли потрібні множинні екземпляри
  • Як заміну глобальних змінних без причини
  • Для складних станів, що залежать від контексту

Висновки

Статичні члени класу — це потужний механізм для зберігання даних і реалізації поведінки, спільної для всіх об'єктів класу. Вони дозволяють інкапсулювати функціональність, яка логічно належить класу, але не прив'язана до конкретних екземплярів.

Ключові моменти, які слід запам'ятати:

  1. Статичне поле: одна копія на весь клас, існує незалежно від об'єктів
  2. Ініціалізація: обов'язково поза класом у .cpp (крім const int і inline static)
  3. Статичний метод: немає this, доступ лише до статичних членів
  4. Доступ: через ім'я класу ClassName::member, а не через об'єкт
  5. Патерни: лічильник об'єктів, генератор ID, фабричні методи, константи

📊 Статичні поля

Спільні дані для всіх об'єктів. Оголошення в класі, визначення в .cpp.

⚙️ Статичні методи

Операції без прив'язки до об'єкта. Немає this, доступ через ClassName::method().

🎯 Практичні застосування

Генератори ID, лічильники, константи, фабричні методи, утилітарні функції.

⚠️ Обережність

Чисто статичні класи — фактично глобальні. Використовуйте обмежено.

На наступному уроці ми розглянемо конструктор копіювання — спеціальний конструктор, що створює новий об'єкт як копію існуючого, та механізм запобігання копіюванню за допомогою = delete.

Copyright © 2026