На цьому уроці ми розглянемо використання статичних змінних-членів класу в мові С++.
Статичні змінні-члени класу
На уроці №54 ми дізналися, що статичні змінні зберігають свої значення і не знищуються навіть після виходу з блоку, в якому вони оголошені, наприклад:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#include <iostream> int generateID() { static int s_id = 0; return ++s_id; } int main() { std::cout << generateID() << '\n'; std::cout << generateID() << '\n'; std::cout << generateID() << '\n'; return 0; } |
Результат виконання програми:
1
2
3
Зверніть увагу, s_id
зберігає своє значення після кожного виклику функції generateID().
Ключове слово static має інше значення, коли мова йде про глобальні змінні — воно надає їм внутрішній зв’язок (що обмежує їх видимість/використання за межами файлу, в якому вони визначені). Оскільки використання глобальних змінних — це зло, то ключове слово static в цьому контексті використовується не дуже часто.
У мові C++ ключове слово static можна використовувати в класах: статичні змінні-члени і статичні методи. Ми поговоримо про статичні змінні-члени на цьому уроці, а про статичні методи — на наступному.
Перш ніж ми перейдемо до ключового слова static зі змінними-членами класу, давайте спочатку розглянемо наступний клас:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
#include <iostream> class Anything { public: int m_value = 3; }; int main() { Anything first; Anything second; first.m_value = 4; std::cout << first.m_value << '\n'; std::cout << second.m_value << '\n'; return 0; } |
При створенні об’єкта класу, кожен об’єкт отримує свою власну копію всіх змінних-членів класу. В цьому випадку, оскільки ми оголосили два об’єкти класу Anything, у нас буде дві копії m_value
: first.m_value
і second.m_value
. Це різні значення, відповідно, результат виконання програми:
4
3
Змінні-члени класу можна зробити статичними, використовуючи ключове слово static. На відміну від звичайних змінних-членів, статичні змінні-члени є загальними для всіх об’єктів класу. Розглянемо наступну програму:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#include <iostream> class Anything { public: static int s_value; }; int Anything::s_value = 3; int main() { Anything first; Anything second; first.s_value = 4; std::cout << first.s_value << '\n'; std::cout << second.s_value << '\n'; return 0; } |
Результат виконання програми:
4
4
Оскільки s_value
є статичною змінною-членом, то вона є спільною для всіх об’єктів класу Anything. Відповідно, first.s_value
— це та ж змінна, що і second.s_value
. Вищенаведена програма показує, що до значення, яке ми встановили через перший об’єкт, можна отримати доступ і через другий об’єкт.
Статичні члени не пов’язані з об’єктами класу
Хоча ви можете отримати доступ до статичних членів через різні об’єкти класу (як у вищенаведеному прикладі), але, виявляється, статичні члени існують, навіть якщо об’єкти класу не створені! Подібно глобальним змінним, вони створюються при запуску програми і знищуються, коли програма завершує своє виконання.
Отже, статичні члени належать класу, а не об’єктам цього класу. Оскільки s_value
існує незалежно від будь-яких об’єктів класу, то доступ до нього здійснюється напряму через ім’я класу і оператор дозволу області видимості (в даному випадку, через Anything::s_value
):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
#include <iostream> class Anything { public: static int s_value; // оголошуємо статичну змінну-член }; int Anything::s_value = 3; // визначаємо статичну змінну-член int main() { // Примітка: Ми не створюємо тут ніяких об'єктів класу Anything Anything::s_value = 4; std::cout << Anything::s_value << '\n'; return 0; } |
У вищенаведеному фрагменті, доступ до s_value
здійснюється через ім’я класу, а не через об’єкт цього класу. Зверніть увагу, ми навіть не створювали об’єкт класу Anything, але ми все одно маємо доступ до Anything::s_value
і можемо використовувати цю змінну-член.
Визначення і ініціалізація статичних змінних-членів класу
Коли ми оголошуємо статичну змінну-член всередині тіла класу, ми повідомляємо компілятору про існування статичної змінної-члена, але не про її визначення (аналогією є попереднє оголошення). Оскільки статичні змінні-члени не є частиною окремих об’єктів класу (вони обробляються аналогічно глобальним змінним і ініціалізуються під час запуску програми), то ви повинні явно визначити статичний член поза тілом класу — в глобальній області видимості.
У вищенаведеній програмі це робиться наступним рядком коду:
1 |
int Anything::s_value = 3; // визначаємо статичну змінну-член |
Тут ми визначили статичну змінну-член класу і ініціалізували її значенням 3
. Якщо ж ініціалізатор не надано, то C++ ініціалізує s_value
значенням 0
.
Зверніть увагу, це визначення статичного члену не підпадає під дію специфікаторів доступу: ви можете визначити і ініціалізувати s_value
, навіть якщо він буде private (або protected).
Якщо клас визначено в заголовку, то визначення статичного члена зазвичай поміщається в файл з кодом класу (наприклад, в Anything.cpp). Якщо клас визначено в файлі .cpp, то визначення статичного члена зазвичай пишеться безпосередньо під класом. Не пишіть визначення статичного члена класу в заголовку (подібно глобальним змінним). Якщо цей заголовок підключають більше одного разу, то ви отримаєте кілька визначень одного члена, що призведе до помилки компіляції.
Ініціалізація статичних змінних-членів всередині тіла класу
Є кілька обхідних шляхів визначення статичних членів всередині тіла класу. По-перше, якщо статичний член є константним інтегральним типом (до якого відносяться і char, і bool) або константним перерахуванням, то статичний член може бути ініціалізований всередині тіла класу:
1 2 3 4 5 |
class Anything { public: static const int s_value = 5; // статичну константну змінну типу int можна оголосити і ініціалізувати напряму }; |
Оскільки тут статична змінна-член є цілочисельною константою, то додаткового рядка явного визначення поза тілом класу вже не потрібно.
По-друге, починаючи з C++11 статичні члени constexpr будь-якого типу даних, що підтримують ініціалізацію constexpr, можуть бути ініціалізовані всередині тіла класу:
1 2 3 4 5 6 7 8 |
#include <array> class Anything { public: static constexpr double s_value = 3.4; // добре static constexpr std::array<int, 3> s_array = { 3, 4, 5 }; // це працює навіть з класами, які підтримують ініціалізацію constexpr }; |
Використання статичних змінних-членів класу
Навіщо використовувати статичні змінні-члени всередині класів? Для присвоювання унікального ідентифікатора кожному об’єкту класу (як варіант):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
#include <iostream> class Anything { private: static int s_idGenerator; int m_id; public: Anything() { m_id = s_idGenerator++; } // збільшуємо значення ідентифікатора для наступного об'єкта int getID() const { return m_id; } }; // Ми визначаємо і ініціалізуємо s_idGenerator незважаючи на те, що він оголошений як private. // Це нормально, оскільки визначення не підпадає під дію специфікаторів доступу int Anything::s_idGenerator = 1; // починаємо наш ID-генератор зі значення 1 int main() { Anything first; Anything second; Anything third; std::cout << first.getID() << '\n'; std::cout << second.getID() << '\n'; std::cout << third.getID() << '\n'; return 0; } |
Результат виконання програми:
1
2
3
Оскільки s_idGenerator
є загальним для всіх об’єктів класу Anything, то при створенні нового об’єкта класу Anything конструктор захоплює поточне значення s_idGenerator
, а потім збільшує його для наступного об’єкта. Це гарантує унікальність ідентифікаторів для кожного створеного об’єкта класу Anything.
Статичні змінні-члени також можуть бути корисні, коли класу необхідно використовувати внутрішню таблицю пошуку (наприклад, масив, який використовується для зберігання набору попередньо обчислених значень). Роблячи таблицю пошуку статичною, для всіх об’єктів класу створиться тільки одна копія (а не окрема для кожного об’єкта класу). Це допоможе заощадити значну кількість пам’яті.