Gdybyś miał poprosić doświadczonego programistę o jedną radę dotyczącą dobrych praktyk programistycznych, po namyśle najbardziej prawdopodobną odpowiedzią byłoby: „Unikaj globalnych zmienne!”. I nie bez powodu: zmienne globalne są jednym z najczęściej nadużywanych pojęć w tym języku. Chociaż mogą wydawać się nieszkodliwe w małych programach akademickich, często stwarzają problemy w większych.
Nowych programistów często kusi, aby używać wielu zmiennych globalnych, ponieważ łatwo jest z nimi pracować, szczególnie gdy w grę wchodzi wiele wywołań różnych funkcji (przekazywanie danych przez parametry funkcji jest uciążliwe). Jednak ogólnie jest to zły pomysł. Wielu programistów uważa, że należy całkowicie unikać zmiennych globalnych innych niż stałe!
Ale zanim przejdziemy do wyjaśnienia, dlaczego. Kiedy programiści mówią Ci, że zmienne globalne są złe, zwykle nie mają na myśli uniknąć zmiennych globalnych. Mówią głównie o zmiennych globalnych o charakterze innym niż stałe.
Dlaczego (inne niż stałe) zmienne globalne są złe
Zdecydowanie największym powodem, dla którego zmienne globalne niebędące stałymi są niebezpieczne, jest to, że ich wartości można zmienić za pomocą dowolnego wywoływanej funkcji, a programista nie ma łatwego sposobu, aby dowiedzieć się, że tak się stanie. Rozważmy następujący program:
#include <iostream>
int g_mode; // declare global variable (will be zero-initialized by default)
void doSomething()
{
g_mode = 2; // set the global g_mode variable to 2
}
int main()
{
g_mode = 1; // note: this sets the global g_mode variable to 1. It does not declare a local g_mode variable!
doSomething();
// Programmer still expects g_mode to be 1
// But doSomething changed it to 2!
if (g_mode == 1)
{
std::cout << "No threat detected.\n";
}
else
{
std::cout << "Launching nuclear missiles...\n";
}
return 0;
}Zauważ, że programista ustawił zmienną g_mode Do 1, a następnie wywołał doSomething(). O ile programista nie miał wyraźnej wiedzy, że doSomething() zamierza zmienić wartość g_mode, prawdopodobnie nie spodziewał się doSomething() zmiany tej wartości! W rezultacie reszta main() nie działa tak, jak tego oczekuje programista (i świat zostaje zniszczony).
W skrócie, zmienne globalne powodują, że stan programu jest nieprzewidywalny. Każde wywołanie funkcji staje się potencjalnie niebezpieczne, a programista nie ma łatwego sposobu, aby dowiedzieć się, które z nich są niebezpieczne, a które nie! Zmienne lokalne są znacznie bezpieczniejsze, ponieważ inne funkcje nie mogą na nie wpływać bezpośrednio.
Istnieje wiele innych dobrych powodów, aby nie używać zmiennych globalnych innych niż stałe.
W przypadku zmiennych globalnych nierzadko można znaleźć fragment kodu wyglądający tak:
void someFunction()
{
// useful code
if (g_mode == 4)
{
// do something good
}
}Po debugowaniu stwierdzasz, że Twój program nie działa poprawnie, ponieważ g_mode ma wartość 3, nie 4. Jak to naprawić? Teraz musisz znaleźć wszystkie miejsca g_mode mogące być ustawione na 3 i prześledzić, jak to się stało. Możliwe, że może to dotyczyć zupełnie niepowiązanego fragmentu kodu!
Jednym z kluczowych powodów, dla których warto deklarować zmienne lokalne jak najbliżej miejsca, w którym są używane, jest to, że minimalizuje to ilość kodu, który trzeba przejrzeć, aby zrozumieć, co robi zmienna. Zmienne globalne znajdują się na drugim końcu spektrum — ponieważ można uzyskać do nich dostęp z dowolnego miejsca, konieczne może być przejrzenie całego programu, aby zrozumieć ich użycie. W małych programach może to nie stanowić problemu. W dużych będzie.
Na przykład możesz znaleźć w swoim programie 442 odniesienia do słowa g_mode . Jeśli g_mode nie jest dobrze udokumentowany, potencjalnie będziesz musiał przejrzeć każde użycie g_mode , aby zrozumieć, w jaki sposób jest on używany w różnych przypadkach, jakie są jego prawidłowe wartości i jaka jest jego ogólna funkcja.
Zmienne globalne sprawiają również, że Twój program jest mniej modułowy i mniej elastyczny. Funkcja, która wykorzystuje tylko swoje parametry i nie ma żadnych skutków ubocznych, jest doskonale modułowa. Modularność pomaga zarówno w zrozumieniu działania programu, jak i możliwości ponownego użycia. Zmienne globalne znacznie zmniejszają modułowość.
W szczególności należy unikać używania zmiennych globalnych dla ważnych zmiennych „punktu decyzyjnego” (np. zmiennych, których można użyć w instrukcji warunkowej, takich jak zmienna g_mode w powyższym przykładzie). Twój program prawdopodobnie nie ulegnie uszkodzeniu, jeśli zmieni się zmienna globalna przechowująca wartość informacyjną (np. nazwa użytkownika). Jest znacznie bardziej prawdopodobne, że ulegnie uszkodzeniu, jeśli zmienisz zmienną globalną, która ma jak faktyczny wpływ na działanie programu.
Najlepsza praktyka
Jeśli to możliwe, używaj zmiennych lokalnych zamiast zmiennych globalnych.
Problem kolejności inicjalizacji zmiennych globalnych
Inicjowanie zmiennych statycznych (w tym zmiennych globalnych) odbywa się w ramach uruchamiania programu, przed wykonaniem funkcji main . Proces ten przebiega w dwóch fazach.
Pierwsza faza nazywana jest inicjalizacją statyczną. Inicjalizacja statyczna przebiega w dwóch fazach:
- Zmienne globalne z inicjatorami constexpr (w tym literały) są inicjalizowane tymi wartościami. Nazywa się to stała inicjalizacja.
- Zmienne globalne bez inicjatorów są inicjowane zerem. Inicjalizacja zerowa jest uważana za formę inicjalizacji statycznej, ponieważ
0jest wartością constexpr.
Druga faza nazywana jest inicjalizacją dynamiczną. Ta faza jest bardziej złożona i pełna niuansów, ale jej istota jest taka, że inicjowane są zmienne globalne z inicjatorami innymi niż constexpr.
Oto przykład inicjatora innego niż constexpr:
int init()
{
return 5;
}
int g_something{ init() }; // non-constexpr initializationW pojedynczym pliku, dla każdej fazy, zmienne globalne są zazwyczaj inicjowane w kolejności definicji (istnieje kilka wyjątków od tej reguły dla fazy dynamicznej inicjalizacji). Biorąc to pod uwagę, należy uważać, aby zmienne nie były zależne od wartości inicjalizacji innych zmiennych, które zostaną zainicjowane dopiero później. Na przykład:
#include <iostream>
int initX(); // forward declaration
int initY(); // forward declaration
int g_x{ initX() }; // g_x is initialized first
int g_y{ initY() };
int initX()
{
return g_y; // g_y isn't initialized when this is called
}
int initY()
{
return 5;
}
int main()
{
std::cout << g_x << ' ' << g_y << '\n';
}Wypisuje:
0 5
Znacznie większym problemem jest niejednoznaczna kolejność inicjowania obiektów statycznych w różnych jednostkach translacji.
Biorąc pod uwagę dwa pliki, a.cpp i b.cpp, w każdym z nich można najpierw zainicjować zmienne globalne. Jeśli jakaś zmienna o statycznym czasie trwania w a.cpp zostanie zainicjalizowana statyczną zmienną czasu trwania zdefiniowaną w b.cpp, istnieje 50% szans, że zmienna w b.cpp nie zostanie jeszcze zainicjowana.
Nomenklatura
Niejednoznaczność kolejności obiektów statycznych inicjowanie czasu przechowywania w różnych jednostkach tłumaczeniowych jest często nazywane fiaskiem kolejności inicjalizacji statycznej.
Ostrzeżenie
Unikaj inicjowania obiektów o statycznym czasie trwania przy użyciu innych obiektów o statycznym czasie trwania z innej jednostki tłumaczenia.
Dynamiczna inicjalizacja zmiennych globalnych jest również podatna na problemy z kolejnością inicjalizacji i należy jej unikać, gdy tylko jest to możliwe.
Jakie są więc bardzo dobre powody, aby używać globali innych niż stałe zmiennych?
Nie ma ich wiele. W większości przypadków preferowane jest używanie zmiennych lokalnych i przekazywanie ich jako argumentów innym funkcjom. Jednak w niektórych przypadkach rozsądne użycie zmiennych globalnych innych niż stałe możesz w rzeczywistości zmniejsza złożoność programu, a w tych rzadkich przypadkach ich użycie może być lepsze niż alternatyw.
Dobrym przykładem jest plik dziennika, w którym można zrzucić informacje o błędach lub debugowaniu. Prawdopodobnie sensowne jest zdefiniowanie tego jako dziennika globalnego, ponieważ prawdopodobnie będziesz mieć tylko jeden taki dziennik w programie i prawdopodobnie będzie on używany w całym programie. Innym dobrym przykładem może być generator liczb losowych (pokazujemy to na lekcji 8.15 -- Globalne liczby losowe (Random.h)).
O ile ma to sens, obiekty std::cout i std::cin są zaimplementowane jako zmienne globalne (wewnątrz std przestrzeni nazw).
Ogólnie rzecz biorąc, każde użycie zmiennej globalnej powinno spełniać co najmniej dwa następujące kryteria: W programie powinna zawsze istnieć tylko jedna rzecz, którą zmienna reprezentuje, a jej użycie powinno być wszechobecne w całym programie programu.
Wielu nowych programistów popełnia błąd sądząc, że coś można zaimplementować globalnie, ponieważ potrzebny jest tylko jeden w tej chwili. Na przykład możesz pomyśleć, że ponieważ wdrażasz grę dla jednego gracza, potrzebujesz tylko jednego gracza. Ale co się stanie później, gdy zechcesz dodać tryb wieloosobowy (w porównaniu do trybu hotseat)?
Chroń się przed globalną zagładą
Jeśli znajdziesz dobre zastosowanie dla zmiennej globalnej o charakterze niestałym, kilka przydatnych porad zminimalizuje ilość kłopotów, w jakie możesz się wpakować. Ta rada nie dotyczy tylko zmiennych globalnych niestałych, ale może być pomocna w przypadku wszystkich zmiennych globalnych.
Najpierw poprzedź wszystkie zmienne globalne nieobjęte przestrzenią nazw przedrostkiem „g” lub „g_”, albo jeszcze lepiej, umieść je w przestrzeni nazw (omówionej w lekcji 7.2 -- Przestrzenie nazw zdefiniowane przez użytkownika i operator rozpoznawania zakresu), aby zmniejszyć ryzyko kolizji nazw.
Na przykład zamiast tego of:
#include <iostream>
constexpr double gravity { 9.8 }; // risk of collision with some other global variable named gravity
int main()
{
std::cout << gravity << '\n'; // unclear if this is a local or global variable from the name
return 0;
}Zrób to:
#include <iostream>
namespace constants
{
constexpr double gravity { 9.8 }; // will not collide with other global variables named gravity
}
int main()
{
std::cout << constants::gravity << '\n'; // clear this is a global variable (since namespaces are global)
return 0;
}Po drugie, zamiast umożliwiać bezpośredni dostęp do zmiennej globalnej, lepszą praktyką jest „hermetyzowanie” zmiennej. Upewnij się, że dostęp do zmiennej można uzyskać tylko z pliku, w którym jest zadeklarowana, np. ustawiając zmienną jako statyczną lub stałą, a następnie udostępnij zewnętrzne globalne „funkcje dostępu” do pracy ze zmienną. Funkcje te mogą zapewnić utrzymanie prawidłowego użytkowania (np. sprawdzanie poprawności danych wejściowych, sprawdzanie zakresu itp.). Ponadto, jeśli kiedykolwiek zdecydujesz się zmienić podstawową implementację (np. przenieść się z jednej bazy danych do drugiej), wystarczy zaktualizować funkcje dostępu zamiast każdego fragmentu kodu, który bezpośrednio używa zmiennej globalnej.
Na przykład zamiast tego:
constants.cpp:
namespace constants
{
extern const double gravity { 9.8 }; // has external linkage, can be accessed by other files
}main.cpp:
#include <iostream>
namespace constants
{
extern const double gravity; // forward declaration
}
int main()
{
std::cout << constants::gravity << '\n'; // direct access to global variable
return 0;
}Zrób to:
contants.cpp:
namespace constants
{
constexpr double gravity { 9.8 }; // has internal linkage, is accessible only within this file
}
double getGravity() // has external linkage, can be accessed by other files
{
// We could add logic here if needed later
// or change the implementation transparently to the callers
return constants::gravity;
} main.cpp:
#include <iostream>
double getGravity(); // forward declaration
int main()
{
std::cout << getGravity() << '\n';
return 0;
}Przypomnienie
Globalny const zmienne mają domyślnie wewnętrzne powiązanie, gravity nie musi być static.
Po trzecie, podczas pisania samodzielnej funkcji, która używa zmiennej globalnej, nie używaj jej bezpośrednio w treści funkcji. Zamiast tego podaj to jako argument. W ten sposób, jeśli w jakiejś sytuacji Twoja funkcja będzie kiedykolwiek musiała użyć innej wartości, możesz po prostu zmienić argument. Pomaga to zachować modułowość.
Zamiast:
#include <iostream>
namespace constants
{
constexpr double gravity { 9.8 };
}
// This function is only useful for calculating your instant velocity based on the global gravity
double instantVelocity(int time)
{
return constants::gravity * time;
}
int main()
{
std::cout << instantVelocity(5) << '\n';
return 0;
}Zrób to:
#include <iostream>
namespace constants
{
constexpr double gravity { 9.8 };
}
// This function can calculate the instant velocity for any gravity value (more useful)
double instantVelocity(int time, double gravity)
{
return gravity * time;
}
int main()
{
std::cout << instantVelocity(5, constants::gravity) << '\n'; // pass our constant to the function as a parameter
return 0;
}żart w C++
Jaki jest najlepszy przedrostek nazewniczy dla zmiennej globalnej?
Odpowiedź: //
Ten żart jest wart wszystkich komentarzy.

