Termin static to jeden z najbardziej mylących terminów w języku C++, głównie dlatego, że static ma różne znaczenia w różnych kontekstach.
W poprzednich lekcjach omawialiśmy, że zmienne globalne mają statyczny czas trwania, co oznacza, że są tworzone podczas uruchamiania programu i niszczone po jego zakończeniu.
Omówiliśmy także, w jaki sposób static słowo kluczowe daje globalnemu identyfikatorowi wewnętrzne powiązanie, co oznacza, że identyfikator może być użyty tylko w pliku, w którym jest zdefiniowany.
W tej lekcji omówimy użycie słowa kluczowego static w zastosowaniu do zmiennej lokalnej.
Statyczne zmienne lokalne
W lekcji >2.5 — Wprowadzenie do zakresu lokalnego, nauczyłeś się, że zmienne lokalne mają domyślnie czas trwania automatyczny, co oznacza, że są one tworzone w momencie definicji i niszczone po wyjściu z bloku.
Korzystając z static słowo kluczowe na zmiennej lokalnej zmienia czas trwania z automatycznego na statyczny. Oznacza to, że zmienna jest teraz tworzona na początku programu i niszczona na końcu programu (podobnie jak zmienna globalna). W rezultacie zmienna statyczna zachowa swoją wartość nawet po wyjściu poza zakres!
Różnicę między zmiennymi lokalnymi o czasie trwania automatycznym i czasie trwania statycznym najłatwiej pokazać na przykładzie.
Czas trwania automatyczny (domyślnie):
#include <iostream>
void incrementAndPrint()
{
int value{ 1 }; // automatic duration by default
++value;
std::cout << value << '\n';
} // value is destroyed here
int main()
{
incrementAndPrint();
incrementAndPrint();
incrementAndPrint();
return 0;
}Za każdym wywołaniem incrementAndPrint() tworzona jest zmienna o nazwie wartość, której przypisuje się wartość 1. incrementAndPrint() zwiększa wartość do 2, a następnie wypisuje wartość 2. Podczas oceny incrementAndPrint() . działanie zostanie zakończone, zmienna wyjdzie poza zakres i zostanie zniszczona. W rezultacie ten program generuje wynik:
2 2 2
Rozważmy teraz wersję tego programu, która używa statycznej zmiennej lokalnej. Jedyna różnica między tym programem a powyższym programem polega na tym, że zmieniliśmy zmienną lokalną z czasu trwania automatycznego na czas trwania statyczny za pomocą static słowo kluczowe.
Statycznego czasu trwania (używając słowa kluczowego static):
#include <iostream>
void incrementAndPrint()
{
static int s_value{ 1 }; // static duration via static keyword. This initializer is only executed once.
++s_value;
std::cout << s_value << '\n';
} // s_value is not destroyed here, but becomes inaccessible because it goes out of scope
int main()
{
incrementAndPrint();
incrementAndPrint();
incrementAndPrint();
return 0;
}W tym programie, ponieważ s_value został zadeklarowany jako static, jest on tworzony na początku programu.
Statyczne zmienne lokalne, które są inicjalizowane zerem lub mają Inicjator constexpr może zostać zainicjowany przy starcie programu.
Statyczne zmienne lokalne, które nie mają inicjatora lub inicjatora innego niż constexpr, są inicjalizowane zerem przy starcie programu. Statyczne zmienne lokalne z inicjatorem innym niż constexpr są ponownie inicjowane przy pierwszym napotkaniu definicji zmiennej. Definicja jest pomijana przy kolejnych wywołaniach, więc nie następuje żadna ponowna inicjalizacja. Ponieważ mają statyczny czas trwania, statyczne zmienne lokalne, które nie są jawnie zainicjowane, zostaną domyślnie zainicjowane zerem.
Ponieważ s_value ma inicjator constexpr 1, s_value zostanie zainicjowany przy uruchomieniu programu.
Po wywołaniu s_value wychodzi poza zakres na końcu funkcji, nie jest niszczony. Za każdym razem funkcja incrementAndPrint() jest wywoływane, wartość s_value pozostaje na tym samym poziomie, na którym ją poprzednio zostawiliśmy. W rezultacie ten program generuje następujące wyniki:
2 3 4
Kluczowa informacja
Statyczne zmienne lokalne są używane, gdy potrzebujesz zmiennej lokalnej do zapamiętania jej wartości podczas wywołań funkcji.
Najlepsza praktyka
Zainicjuj swoje statyczne zmienne lokalne. Statyczne zmienne lokalne są inicjowane tylko przy pierwszym wykonaniu kodu, a nie przy kolejnych wywołaniach.
Wskazówka
Podobnie jak używamy „g_” do poprzedzania zmiennych globalnych, często używa się „s_” do poprzedzania statycznych zmiennych lokalnych (czas trwania statycznego).
Generowanie identyfikatora
Jednym z najpowszechniejszych zastosowań zmiennych lokalnych o statycznym czasie trwania są generatory unikalnych identyfikatorów. Wyobraź sobie program, w którym masz wiele podobnych obiektów (np. grę, w której atakuje Cię wiele zombie lub symulację, w której wyświetlasz wiele trójkątów). Jeśli zauważysz defekt, rozróżnienie, który obiekt powoduje problemy, może być prawie niemożliwe. Jeśli jednak każdemu obiektowi przy tworzeniu zostanie nadany unikalny identyfikator, rozróżnienie obiektów na potrzeby dalszego debugowania może być łatwiejsze.
Generowanie unikalnego numeru identyfikacyjnego jest bardzo łatwe przy użyciu zmiennej lokalnej o statycznym czasie trwania:
int generateID()
{
static int s_itemID{ 0 };
return s_itemID++; // makes copy of s_itemID, increments the real s_itemID, then returns the value in the copy
}Przy pierwszym wywołaniu tej funkcji zwracana jest 0. Za drugim razem zwraca 1. Za każdym razem, gdy jest wywoływana, zwraca liczbę o jeden wyższą niż poprzednio wywołana. Możesz przypisać te numery jako unikalne identyfikatory dla swoich obiektów. Ponieważ s_itemID jest zmienną lokalną, nie można jej „manipulować” przez inne funkcje.
Zmienne statyczne oferują pewne korzyści zmiennych globalnych (nie ulegają zniszczeniu aż do końca programu), ograniczając jednocześnie ich widoczność do zakresu blokowego. Dzięki temu są łatwiejsze do zrozumienia i bezpieczniejsze w użyciu.
Kluczowa informacja
Statyczna zmienna lokalna ma zasięg blokowy jak zmienna lokalna, ale jej czas życia trwa do końca programu jak zmienna globalna.
Statyczne stałe lokalne
Statyczne zmienne lokalne można przekształcić w const (lub constexpr). Dobrym zastosowaniem stałej statycznej zmiennej lokalnej jest sytuacja, gdy masz funkcję, która musi używać wartości stałej, ale utworzenie lub inicjowanie obiektu jest kosztowne (np. musisz odczytać wartość z bazy danych). Jeśli użyłbyś normalnej zmiennej lokalnej, zmienna zostałaby utworzona i zainicjowana za każdym razem, gdy funkcja została wykonana. Dzięki statycznej zmiennej lokalnej const/constexpr możesz raz utworzyć i zainicjować kosztowny obiekt, a następnie użyć go ponownie przy każdym wywołaniu funkcji.
Kluczowa informacja
Statycznych zmiennych lokalnych najlepiej używać, aby uniknąć kosztownej inicjalizacji obiektu lokalnego przy każdym wywołaniu funkcji.
Nie używaj statycznych zmiennych lokalnych do zmiany przepływu
Rozważ następujący kod:
#include <iostream>
int getInteger()
{
static bool s_isFirstCall{ true };
if (s_isFirstCall)
{
std::cout << "Enter an integer: ";
s_isFirstCall = false;
}
else
{
std::cout << "Enter another integer: ";
}
int i{};
std::cin >> i;
return i;
}
int main()
{
int a{ getInteger() };
int b{ getInteger() };
std::cout << a << " + " << b << " = " << (a + b) << '\n';
return 0;
}Przykładowe wyjście
Enter an integer: 5 Enter another integer: 9 5 + 9 = 14
Ten kod robi to, co powinien zrobić, ale ponieważ użyliśmy statycznej zmiennej lokalnej, utrudniliśmy zrozumienie kodu. Jeśli ktoś czyta kod w main() bez zapoznania się z implementacją getInteger(), nie będzie miał powodu zakładać, że te dwa wywołania getInteger() zrobią coś innego. Jednak te dwa połączenia powodują coś innego, co może być bardzo mylące, jeśli różnica jest większa niż zmieniony monit.
Załóżmy, że naciskasz przycisk +1 na kuchence mikrofalowej, a kuchenka mikrofalowa dodaje 1 minutę do pozostałego czasu. Twój posiłek jest ciepły i jesteś szczęśliwy. Zanim wyjmiesz posiłek z kuchenki, widzisz za oknem kota i przyglądasz mu się przez chwilę, bo koty są fajne. Ta chwila okazała się dłuższa, niż się spodziewałeś, a kiedy bierzesz pierwszy kęs posiłku, znów jest zimno. Nie ma problemu, po prostu włóż go z powrotem do kuchenki mikrofalowej i naciśnij +1, aby uruchomić go na minutę. Ale tym razem kuchenka mikrofalowa dodaje tylko 1 sekundę, a nie 1 minutę. Wtedy mówisz: „Nic nie zmieniłem i teraz jest zepsute” lub „Ostatnim razem zadziałało”. Jeśli zrobisz to samo jeszcze raz, możesz spodziewać się takiego samego zachowania jak ostatnim razem. To samo tyczy się funkcji.
Załóżmy, że chcemy dodać odejmowanie do kalkulatora tak, aby wynik wyglądał następująco:
Addition Enter an integer: 5 Enter another integer: 9 5 + 9 = 14 Subtraction Enter an integer: 12 Enter another integer: 3 12 - 3 = 9
Możemy spróbować użyć getInteger() do wczytania dwóch kolejnych liczb całkowitych, tak jak to zrobiliśmy w przypadku dodawania.
int main()
{
std::cout << "Addition\n";
int a{ getInteger() };
int b{ getInteger() };
std::cout << a << " + " << b << " = " << (a + b) << '\n';
std::cout << "Subtraction\n";
int c{ getInteger() };
int d{ getInteger() };
std::cout << c << " - " << d << " = " << (c - d) << '\n';
return 0;
}Ale to nie przyniesie oczekiwanych rezultatów, ponieważ wynik będzie następujący:
Addition Enter an integer: 5 Enter another integer: 9 5 + 9 = 14 Subtraction Enter another integer: 12 Enter another integer: 3 12 - 3 = 9
(Linia trzecia od końca jest „Wprowadź inną liczbę całkowitą” zamiast „Wprowadź liczbę całkowitą”)
getInteger() nie nadaje się do ponownego użycia, ponieważ ma stan wewnętrzny (statyczna zmienna lokalna s_isFirstCall), którego nie można zresetować z zewnątrz. s_isFirstCall nie jest zmienną, która powinna być unikalna w całym programie. Chociaż nasz program działał świetnie, kiedy go napisaliśmy, statyczna zmienna lokalna uniemożliwia nam późniejsze ponowne użycie tej funkcji.
Jeden lepszy sposób implementacji getInteger jest przekazanie s_isFirstCall jako parametru. Pozwala to wywołującemu wybrać, który monit zostanie wydrukowany:
#include <iostream>
// We'll define a symbolic constant with a nice name
constexpr bool g_firstCall { true };
int getInteger(bool bFirstCall)
{
if (bFirstCall)
{
std::cout << "Enter an integer: ";
}
else
{
std::cout << "Enter another integer: ";
}
int i{};
std::cin >> i;
return i;
}
int main()
{
int a{ getInteger(g_firstCall) }; // so that it's clearer what the argument represents here
int b{ getInteger(!g_firstCall) };
std::cout << a << " + " << b << " = " << (a + b) << '\n';
return 0;
}Niestałe statyczne zmienne lokalne powinny być używane tylko wtedy, gdy w całym programie i w dającej się przewidzieć przyszłości programu zmienna jest unikalna i nie ma sensu resetowanie zmiennej.
Najlepsza praktyka
Statyczne zmienne lokalne są generalnie w porządku.
Zasadniczo należy unikać statycznych zmiennych lokalnych niebędących stałymi. Jeśli ich użyjesz, upewnij się, że zmienna nigdy nie musi być resetowana i nie jest używana do zmiany przebiegu programu.
Wskazówka
Jeszcze bardziej nadającym się do ponownego użycia rozwiązaniem byłaby zmiana parametru bool na std::string_view i umożliwienie wywołującemu przekazania podpowiedzi tekstowej, która będzie używana!
Dla zaawansowanych czytelników
W przypadkach, gdy potrzebujesz wielu wystąpień zmiennej innej niż stała, która zapamiętuje jej wartość (np. aby mieć wiele identyfikatorów generatory), funktor jest dobrym rozwiązaniem (zobacz lekcję 21.10 -- Przeciążanie operatora nawiasów).
Czas quizu
Pytanie nr 1
Jaki wpływ ma użycie słowa kluczowego static na zmienną globalną? Jaki wpływ ma to na zmienną lokalną?

