Статичні члени класу
Статичні члени класу
Дані об'єкта проти даних класу
До цього моменту всі поля класів, які ми оголошували, належали конкретним об'єктам. Коли ви створюєте об'єкт 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; // Статичне поле
};
Приклад: звичайне поле проти статичного
#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;
}
Що показує приклад
Звичайне поле normalValue:
c1.normalValue = 10впливає лише наc1c2.normalValue = 20впливає лише наc2- Два об'єкти — дві незалежні змінні
Статичне поле staticValue:
c1.staticValue = 100змінює єдину зміннуCounter::staticValuec2.staticValueчитає ту саму змінну — значення100c2.staticValue = 200змінює ту саму зміннуc1.staticValueтепер також показує200
Обидва c1.staticValue і c2.staticValue — це різні імена для однієї й тієї ж змінної.
Доступ до статичних полів: через клас, а не через об'єкт
Хоча до статичного поля можна звертатися через об'єкт (c1.staticValue), правильніше та явніше звертатися через ім'я класу:
Counter::staticValue = 100;
std::cout << Counter::staticValue << "\n";
Це підкреслює, що поле належить класу, а не конкретному об'єкту.
#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::обов'язковий — він вказує, що змінна належить класу.
Приклад: заголовок і реалізація
#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
#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;
}
#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;
}
Аналіз прикладу
Рядок 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
#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;
}
Чому метод статичний?
- Генератор ID не залежить від конкретного об'єкта
- Не потрібно створювати екземпляр класу для отримання ID
- Метод працює лише зі статичним полем
nextID
Визначення статичних методів поза класом
Як і звичайні методи, статичні можна визначати поза тілом класу. Важливо: ключове слово static використовується лише в оголошенні, не в визначенні.
#ifndef MATH_H
#define MATH_H
class Math
{
public:
static int add(int a, int b);
static int multiply(int a, int b);
};
#endif
#include "Math.h"
// Без слова static у визначенні!
int Math::add(int a, int b)
{
return a + b;
}
int Math::multiply(int a, int b)
{
return a * b;
}
#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, де кожен об'єкт автоматично отримує унікальний ідентифікатор при створенні.
#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
#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";
}
#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;
}
Як це працює
Статичне поле 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() нестатичний
}
};
class Example
{
private:
int value;
static int sharedValue;
public:
static void printSharedValue()
{
std::cout << sharedValue << "\n"; // ✅ Статичне поле
}
static void setSharedValue(int v)
{
sharedValue = v; // ✅ Статичне поле
}
static void compute(int a, int b)
{
// ✅ Робота з параметрами
std::cout << a + b << "\n";
}
};
- Статичних полів класу
- Інших статичних методів класу
- Своїх параметрів
- Локальних змінних
Візуалізація пам'яті: звичайні 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);
Проблеми чисто статичних класів
⚠️ Неможливість множинних екземплярів
🌐 Фактично глобальні
🔒 Складність тестування
Коли чисто статичні класи прийнятні
- Утилітарні функції без стану:
MathUtils,StringHelper - Константи та конфігурація:
Config::MAX_CONNECTIONS - Фабричні методи:
Object::create()
Статичні константи: практичний приклад
#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.
Практичне завдання: клас BankAccount з генератором номерів
Створіть клас BankAccount із такими вимогами:
Поля:
accountNumber(int) — унікальний номер рахункуownerName(std::string) — ім'я власникаbalance(double) — баланс рахункуnextAccountNumber(static int) — генератор номерів
Методи:
- Конструктор з параметрами: ім'я власника, початковий баланс (за замовчуванням 0)
deposit(double amount)— поповнення рахункуwithdraw(double amount)— зняття коштів (з перевіркою балансу)print() const— вивід інформації про рахунокgetTotalAccounts()(static) — кількість створених рахунків
Тестування:
- Створіть 3 рахунки з різними власниками
- Виконайте кілька операцій deposit/withdraw
- Виведіть інформацію про всі рахунки
- Покажіть загальну кількість рахунків
BankAccount.h:
#ifndef BANKACCOUNT_H
#define BANKACCOUNT_H
#include <string>
class BankAccount
{
private:
int accountNumber;
std::string ownerName;
double balance;
static int nextAccountNumber;
public:
BankAccount(const std::string& name, double initialBalance = 0.0);
void deposit(double amount);
bool withdraw(double amount);
void print() const;
int getAccountNumber() const { return accountNumber; }
double getBalance() const { return balance; }
static int getTotalAccounts() { return nextAccountNumber - 1000; }
};
#endif
BankAccount.cpp:
#include "BankAccount.h"
#include <iostream>
#include <iomanip>
int BankAccount::nextAccountNumber = 1000; // Починаємо з 1000
BankAccount::BankAccount(const std::string& name, double initialBalance)
: ownerName(name), balance(initialBalance)
{
accountNumber = nextAccountNumber++;
if (balance < 0.0) {
balance = 0.0;
}
}
void BankAccount::deposit(double amount)
{
if (amount > 0) {
balance += amount;
std::cout << "Поповнено рахунок #" << accountNumber
<< " на " << amount << " грн\n";
} else {
std::cout << "Некоректна сума для поповнення\n";
}
}
bool BankAccount::withdraw(double amount)
{
if (amount <= 0) {
std::cout << "Некоректна сума для зняття\n";
return false;
}
if (amount > balance) {
std::cout << "Недостатньо коштів на рахунку #" << accountNumber << "\n";
return false;
}
balance -= amount;
std::cout << "Знято з рахунку #" << accountNumber
<< " суму " << amount << " грн\n";
return true;
}
void BankAccount::print() const
{
std::cout << std::fixed << std::setprecision(2);
std::cout << "Рахунок #" << accountNumber << "\n";
std::cout << " Власник: " << ownerName << "\n";
std::cout << " Баланс: " << balance << " грн\n";
}
main.cpp:
#include <iostream>
#include "BankAccount.h"
int main()
{
std::cout << "=== Банківська система ===\n\n";
BankAccount acc1("Олександр Петренко", 5000.0);
BankAccount acc2("Марія Іваненко", 3000.0);
BankAccount acc3("Ігор Сидоренко");
std::cout << "\n--- Початковий стан ---\n";
acc1.print();
std::cout << "\n";
acc2.print();
std::cout << "\n";
acc3.print();
std::cout << "\n--- Операції ---\n";
acc1.deposit(1500.0);
acc2.withdraw(500.0);
acc3.deposit(2000.0);
acc1.withdraw(2000.0);
std::cout << "\n--- Спроба зняти більше, ніж є ---\n";
acc2.withdraw(5000.0);
std::cout << "\n--- Фінальний стан ---\n";
acc1.print();
std::cout << "\n";
acc2.print();
std::cout << "\n";
acc3.print();
std::cout << "\n=========================\n";
std::cout << "Всього створено рахунків: " << BankAccount::getTotalAccounts() << "\n";
return 0;
}
Компіляція та запуск:
Аналіз:
✅ Статичне поле nextAccountNumber:
- Генерує унікальні номери рахунків
- Починається з 1000
- Автоматично інкрементується в конструкторі
✅ Статичний метод getTotalAccounts():
- Повертає кількість створених рахунків
- Не потребує створення об'єкта для виклику
✅ Кожен об'єкт має унікальний accountNumber:
- acc1 → 1000
- acc2 → 1001
- acc3 → 1002
Підсумкова таблиця: статичні члени класу
| Аспект | Звичайний член | Статичний член |
|---|---|---|
| Належить | Конкретному об'єкту | Класу загалом |
| Кількість копій | По одній на кожен об'єкт | Одна на весь клас |
| Створення | При створенні об'єкта | При запуску програми |
| Доступ | object.member | ClassName::member (рекомендовано) |
this у методах | Є | Немає |
| Доступ до інших членів | До всіх | Лише до статичних |
| Ініціалізація | У конструкторі | Поза класом (.cpp) |
| Виняток ініціалізації | — | const int у класі, inline static (C++17) |
Коли використовувати статичні члени
✅ Використовуйте статичні поля
- Лічильник об'єктів класу
- Генератор унікальних ID
- Конфігураційні константи
- Спільні ресурси (підключення до БД, кеш)
- Дані, що не залежать від конкретного об'єкта
✅ Використовуйте статичні методи
- Фабричні методи створення об'єктів
- Утилітарні функції без стану
- Доступ до статичних полів
- Операції, що не потребують об'єкта
❌ Не використовуйте статичні члени
- Для даних, специфічних для об'єкта
- Коли потрібні множинні екземпляри
- Як заміну глобальних змінних без причини
- Для складних станів, що залежать від контексту
Висновки
Статичні члени класу — це потужний механізм для зберігання даних і реалізації поведінки, спільної для всіх об'єктів класу. Вони дозволяють інкапсулювати функціональність, яка логічно належить класу, але не прив'язана до конкретних екземплярів.
Ключові моменти, які слід запам'ятати:
- Статичне поле: одна копія на весь клас, існує незалежно від об'єктів
- Ініціалізація: обов'язково поза класом у
.cpp(крімconst intіinline static) - Статичний метод: немає
this, доступ лише до статичних членів - Доступ: через ім'я класу
ClassName::member, а не через об'єкт - Патерни: лічильник об'єктів, генератор ID, фабричні методи, константи
📊 Статичні поля
.cpp.⚙️ Статичні методи
this, доступ через ClassName::method().🎯 Практичні застосування
⚠️ Обережність
На наступному уроці ми розглянемо конструктор копіювання — спеціальний конструктор, що створює новий об'єкт як копію існуючого, та механізм запобігання копіюванню за допомогою = delete.
Класи та const
Константні об'єкти класів, константні методи та їхній зв'язок із this, правило «всі геттери — const», передача об'єктів по константному посиланню, перевантаження за const, практичні патерни використання const у класах.
Конструктор копіювання та запобігання копіюванню
Коли C++ копіює об'єкти, неявний конструктор копіювання, сигнатура та семантика, три сценарії копіювання (ініціалізація, передача за значенням, повернення), проблема shallow copy із динамічною пам'яттю, заборона копіювання через = delete.