7.8 — Dlaczego (inne niż stałe) zmienne globalne są złe

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; // deklaruj zmienną globalną (domyślnie będzie inicjalizowana zerem)

void doSomething()
{
    g_mode = 2; // ustaw globalną zmienną g_mode na 2
}

int main()
{
    g_mode = 1; // uwaga: ustawia globalną zmienną g_mode na 1. Nie deklaruje lokalnej zmiennej g_mode!

    doSomething();

    // Programista nadal oczekuje, że g_mode będzie miał wartość 1
    // Ale doSomething zmienił to na 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)
    {
        // zrobić coś dobrego
    }
}

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ż 0 jest 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() }; // Inicjalizacja inna niż constexpr

W 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 jest inicjalizowane jako pierwsze
int g_y{ initY() };

int initX()
{
    return g_y; // g_y nie jest inicjalizowane, gdy jest to wywoływany
}

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 }; // ryzyko kolizji z inną zmienną globalną o nazwie grawitacja

int main()
{
    std::cout << gravity << '\n'; // nie jest jasne, czy jest zmienna lokalna czy globalna z nazwą

    return 0;
}

Zrób to:

#include <iostream>

namespace constants
{
    constexpr double gravity { 9.8 }; // nie będzie kolidować z innymi zmiennymi globalnymi zwanymi grawitacją
}

int main()
{
    std::cout << constants::gravity << '\n'; // wyczyść to zmienna globalna (ponieważ przestrzenie nazw są globalne)

    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 }; // posiada zewnętrzne powiązanie, można uzyskać do niego dostęp z innych plików
}

main.cpp:

#include <iostream>

namespace constants
{
    extern const double gravity; // forward declaration
}

int main()
{
    std::cout << constants::gravity << '\n'; // bezpośredni dostęp do zmiennej globalnej

    return 0;
}

Zrób to:

contants.cpp:

namespace constants
{
    constexpr double gravity { 9.8 }; // posiada wewnętrzne powiązanie, jest dostępny tylko w tym pliku
}

double getGravity() // posiada zewnętrzne powiązanie, można uzyskać do niego dostęp z innych plików
{
    // Możemy dodać tutaj logikę, jeśli zajdzie taka potrzeba później
    // lub zmień implementację w sposób przejrzysty dla wywołujących
    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 };
}

// Ta funkcja jest przydatne tylko do obliczenia prędkości chwilowej na podstawie globalnej grawitacji
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 };
}

// Ta funkcja może obliczyć prędkość chwilową dla dowolnej wartości grawitacji (bardziej przydatne)
double instantVelocity(int time, double gravity)
{
    return gravity * time;
}

int main()
{
    std::cout << instantVelocity(5, constants::gravity) << '\n'; // przekazuje naszą stałą do funkcji jako parametr

    return 0;
}

żart w C++

Jaki jest najlepszy przedrostek nazewniczy dla zmiennej globalnej?

Odpowiedź: //

Ten żart jest wart wszystkich komentarzy.

guest
Twój adres e-mail nie zostanie wyświetlony
Znalazłeś błąd? Zostaw komentarz powyżej!
Komentarze związane z poprawkami zostaną usunięte po przetworzeniu, aby pomóc zmniejszyć bałagan. Dziękujemy za pomoc w ulepszaniu witryny dla wszystkich!
Awatary z https://gravatar.com/ są połączone z podanym adresem e-mail.
Powiadamiaj mnie o odpowiedziach:  
348 Komentarze
Najnowsze
Najstarsze Najczęściej głosowane
Wbudowane opinie
Wyświetl wszystkie komentarze