Якщо ви запитаєте ветерана-програміста дати одну слушну пораду по програмуванню, то після певних роздумів він відповість: «Уникайте використання глобальних змінних!». І, частково, він матиме рацію. Глобальні змінні є одними з найбільш зловживаних об’єктів в мові C++. Хоч вони і не виглядають проблемними в невеликих програмах, у великих проектах зазвичай все навпаки.
Початківці часто використовують величезну кількість глобальних змінних, тому що з ними легко працювати, особливо коли задіяно багато функцій. Це погана ідея. Багато розробників вважають, що неконстантні глобальні змінні взагалі не слід використовувати!
Але перш ніж ми розберемося з питанням «Чому?», потрібно дещо уточнити. Коли розробники кажуть, що глобальні змінні — це зло, вони не мають на увазі повністю ВСІ глобальні змінні. Вони говорять про неконстантні глобальні змінні.
Чому (неконстантні) глобальні змінні — це зло?
Безумовно, причиною №1, чому неконстантні глобальні змінні є небезпечними — це те, що їх значення може змінити будь-яка функція, яка викликається і ви навіть можете цього не знати. Наприклад, розглянемо наступну програму:
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 |
#include <iostream> // Оголошення глобальної змінної int g_mode; void doSomething() { g_mode = 2; // присвоюємо глобальній змінній g_mode значення 2 } int main() { g_mode = 1; // примітка: тут ми присвоюємо глобальній змінній g_mode значення 1. Це не оголошення локальної змінної g_mode! doSomething(); // Програміст, як і раніше, очікує, що g_mode матиме значення 1. // Але функція doSomething() змінила значення цієї змінної на 2! if (g_mode == 1) std::cout << "No threat detected.\n"; else std::cout << "Launching nuclear missiles...\n"; return 0; } |
Результат виконання програми:
Launching nuclear missiles...
Спочатку ми присвоюємо змінній g_mode
значення 1
, а потім викликаємо функцію doSomething(). Якби ми не знали заздалегідь, що doSomething() змінить значення g_mode
, то, ймовірно, не очікували б подальшого розвитку подій (g_mode = 2
=> Запуск ядерних боєголовок...
)!
Неконстантні глобальні змінні роблять кожну функцію потенційно небезпечною, і програміст не може знати заздалегідь, яка з використовуваних ним функцій є небезпечною, а яка — ні. Локальні змінні набагато безпечніші через те, що інші функції не можуть впливати на них напряму.
Також є багато інших вагомих причин не використовувати неконстантні глобальні змінні.
Наприклад, нерідко можна зустріти приблизно наступне:
1 2 3 4 5 6 |
void boo() { // Деякий код if (g_mode == 4) // робимо що-небудь корисне } |
Припустимо, що g_mode
дорівнює 3
, а не 4
— наша програма видасть неправильні результати. Як це виправити? Потрібно буде відшукати усі місця, де, ймовірно, могло змінитися значення змінної g_mode
, а потім простежити хід виконання коду в кожній потенційно небезпечній ділянці. Можливо, зміну глобальної змінної ви виявите взагалі в іншому коді, який, як вам здалося на перший погляд, ніяк не був пов’язаний з фрагментом, наведеним вище.
Однією з причин оголошення локальних змінних максимально близько до місця їх першого використання є зменшення кількості коду, який потрібно буде переглянути, щоб зрозуміти, що робить (навіщо потрібна) змінна. З глобальними змінними справи йдуть трохи інакше — оскільки їх можна використовувати в будь-якому місці програми, то вам доведеться переглянути мало не весь код, щоб простежити логіку виконання і зміни значень змінних у вашій програмі.
Наприклад, ви можете виявити, що на g_mode
посилаються 442 рази в вашій програмі. Якщо використання змінної g_mode
не підкріплене коментарями, то вам доведеться переглянути кожну згадку g_mode
, щоб зрозуміти, як вона використовується в різних випадках.
Також глобальні змінні роблять вашу програму менш модульною та гнучкою. Функція, яка використовує тільки свої параметри і не має побічних ефектів, є ідеальною в плані модульності. Модульність допомагає зрозуміти структуру вашої програми, що вона робить і як можна повторно використовувати певні ділянки коду в іншій програмі. Глобальні змінні значно зменшують цю можливість.
Зокрема, не використовуйте глобальні змінні в якості важливих змінних, які виконують головні або вирішальні функції в програмі (наприклад, змінні, які використовуються в умовних стейтментах, як g_mode
вище). Ваша програма навряд чи зламається, якщо в ній буде глобальна змінна з інформаційним значенням, яке може змінюватися (наприклад, ім’я користувача). Набагато гірше, якщо зміниться значення глобальної змінної, яка впливає безпосередньо на результати виконання самої програми або на її роботу.
Правило: Замість глобальних змінних використовуйте локальні (коли це доцільно).
У чому плюси використання (неконстантних) глобальних змінних?
Їх не багато. В основному можна обійтися без використання неконстантних глобальних змінних. Але в деяких випадках їх розумне використання може зменшити складність програми, і, іноді, може бути навіть кращим, ніж альтернативні варіанти вирішення проблеми.
Наприклад, якщо ваша програма використовує базу даних для читання і запису даних, то є сенс визначити базу даних глобально, оскільки доступ до неї може знадобитися з будь-якого місця. Аналогічно, якщо у вашій програмі є журнал помилок (або журнал відлагодження), в якому ви можете читати/записувати інформацію про помилки (або про відлагодження), то є сенс визначити його глобально. Звукова бібліотека може бути ще одним хорошим прикладом: вам, ймовірно, не захочеться відкривати доступ до неї для кожної функції, яка робить запит. Оскільки у вас буде тільки одна звукова бібліотека, яка управляє всіма звуками, логічно буде оголосити її глобально, ініціалізувати під час запуску програми, а потім використовувати тільки в режимі читання.
Як захиститися від “глобального руйнування”?
Якщо у вас виникне ситуація, в якій краще буде використовувати неконстантні глобальні змінні, замість локальних, то ось вам декілька порад, які допоможуть звести до мінімуму кількість потенційних проблем, з якими ви можете зіткнутися при використанні подібних змінних.
По-перше, додавайте префікс g_
до всіх глобальних змінних і/або розміщуйте їх в просторі імен, щоб зменшити ймовірність виникнення конфліктів імен.
Наприклад, замість наступного:
1 2 3 4 5 6 7 8 |
#include <iostream> double gravity (9.8); // по імені змінної незрозуміло, глобальна ця змінна чи локальна int main() { return 0; } |
Зробіть наступне:
1 2 3 4 5 6 7 8 |
#include <iostream> double g_gravity (9.8); // тепер зрозуміло, що це глобальна змінна int main() { return 0; } |
По-друге, замість дозволу прямого доступу до глобальних змінних, краще їх інкапсулювати. Спочатку додайте ключове слово static
, щоб доступ до них був можливий тільки з файлу, в якому вони оголошені. Потім напишіть зовнішні глобальні «функції доступу» для роботи зі змінними. Ці функції допоможуть забезпечити належне використання змінних (наприклад, при перевірці користувацького вводу чи допустимого діапазону значень тощо). Крім того, якщо ви коли-небудь вирішите змінити початкову реалізацію програми (наприклад, перейти з однієї бази даних в іншу), то вам потрібно буде оновити тільки функції доступу замість кожного фрагменту коду, який безпосередньо використовує глобальні змінні.
Наприклад, замість наступного:
1 |
double g_gravity (9.8); // можна експортувати і використовувати напряму в будь-якому файлі |
Зробіть наступне:
1 2 3 4 5 6 |
static double g_gravity (9.8); // обмежуємо доступ до змінної (тільки для цього файлу) double getGravity() // цю функцію можна експортувати в інші файли для доступу до глобальної змінної { return g_gravity; } |
По-третє, при написанні автономної функції, яка використовує глобальні змінні, не використовуйте їх безпосередньо в тілі функції. Передавайте їх в якості параметрів. Таким чином, якщо у вашій функції потрібно буде коли-небудь використати інше значення, то ви зможете просто змінити параметр. Це підвищить модульність вашої програми.
Наприклад, замість наступного:
1 2 3 4 5 |
// Ця функція корисна тільки для розрахунку миттєвої швидкості на основі глобальної гравітації double instantVelocity(int time) { return g_gravity * time; } |
Зробіть наступне:
1 2 3 4 5 6 |
// Ця функція обчислює миттєву швидкість для будь-якого значення гравітації. // Передайте значення, що повертається, з getGravity() в параметр gravity, якщо хочете використовувати глобальну змінну gravity double instantVelocity(int time, double gravity) { return gravity * time; } |
Нарешті, зміна значень глобальних змінних — це прямий шлях до проблем. Структуруйте ваш код відповідно до того факту, що ваші глобальні змінні можуть змінитися. Постарайтеся звести до мінімуму кількість випадків, де вони можуть змінювати свої значення — використовуйте їх тільки як “доступні тільки для читання” (наскільки це можливо). Якщо ви можете ініціалізувати значення глобальної змінної при запуску програми, а потім не змінювати його в ході виконання, то, таким чином, ви знизите ймовірність виникнення непередбачуваних проблем.
Жарт
Який найкращий префікс для глобальних змінних?
Відповідь: //
.
Висновки
Уникайте використання неконстантних глобальних змінних, наскільки це можливо! Якщо ж використовуєте, то використовуйте їх максимально розумно і обережно.
Ахах, і справді, глобальні змінні – зло))