Dwa pytania o każdą zmienną
Każda zmienna w C ma dwie osobne właściwości, a ich mylenie jest źródłem większości niespodzianek z tej strony:
- Zasięg: gdzie można użyć nazwy. Pytanie dotyczące czasu kompilacji.
- Czas życia: jak długo istnieje pamięć. Pytanie dotyczące czasu działania.
Zwykle idą w parze (zwykła zmienna lokalna jest widoczna w swoim bloku i żyje dokładnie tak długo), ale static je rozdziela, i dlatego właśnie to słowo kluczowe warto tu zrozumieć.
Zasięg blokowy
Zmienna zadeklarowana wewnątrz nawiasów klamrowych jest widoczna od deklaracji do nawiasu zamykającego i nigdzie indziej.
Wewnętrzny blok widzi outer, bo jest w nim zagnieżdżony. Zewnętrzny blok nie widzi inner. Dotyczy to każdej pary nawiasów klamrowych: ciał funkcji, ciał if, ciał pętli albo samodzielnego bloku, takiego jak ten powyżej.
Pętle dobrze to pokazują. Licznik zadeklarowany w nagłówku for należy do pętli:
for (int i = 0; i < 5; i++) {
/* tutaj i zyje */
}
/* tutaj i nie istnieje */
I dokładnie o to chodzi: dwie pętle w tej samej funkcji mogą używać i bez wzajemnego wpływu, a licznika nie da się przypadkiem odczytać po pętli. Jeśli potrzebujesz tej wartości później, na przykład indeksu, na którym zatrzymało się wyszukiwanie, zadeklaruj ją przed pętlą.
Parametry funkcji mają zasięg ciała funkcji, więc zachowują się jak zmienne lokalne zadeklarowane na jego początku.
C99 pozwala umieścić deklarację w dowolnym miejscu bloku, nie tylko na początku. Deklaruj zmienne tam, gdzie są potrzebne po raz pierwszy: zmienna o krótkim zasięgu to zmienna, która ma mniej sposobów, by coś popsuć.
Czas życia: pamięć automatyczna
Zwykła zmienna lokalna ma pamięć automatyczną: powstaje, gdy sterowanie wchodzi do jej bloku, i jest niszczona, gdy z niego wychodzi. Jej pamięć znajduje się na stosie.
Program trzy razy wypisuje 1. Każde wywołanie dostaje zupełnie nowe count, zainicjalizowane na 0 i wyrzucane przy powrocie. Z tego samego powodu każda ramka funkcji rekurencyjnej ma własną kopię każdej zmiennej lokalnej.
Dwie konsekwencje warto powiedzieć wprost. Niezainicjalizowana zmienna lokalna zawiera śmieci, a nie zero, czyli bajty, które akurat były na stosie. A zwrócenie wskaźnika na zmienną lokalną to niezdefiniowane zachowanie, bo pamięć znika w chwili powrotu z funkcji:
/* BLAD: buf nie istnieje po return */
char *broken(void) {
char buf[64] = "hello";
return buf;
}
Lokalne zmienne static: pamięć między wywołaniami
Dodaj static do zmiennej lokalnej, a jej czas życia obejmie cały program, podczas gdy zasięg pozostanie dokładnie taki sam.
Teraz licznik wypisuje 1, 2, 3. = 0 wykonuje się raz, przed startem main, a nie przy każdym wywołaniu.
Trzy zasady dotyczące lokalnych zmiennych static:
- Inicjalizator musi być wyrażeniem stałym, bo jest stosowany na starcie programu, a nie w czasie działania.
- Bez inicjalizatora zmienna
staticjest inicjalizowana zerem (w przeciwieństwie do automatycznej zmiennej lokalnej, która zawiera śmieci). - Zmienna nadal jest prywatna dla funkcji. Żaden inny kod nie może się do niej odwołać po nazwie.
Ten ostatni punkt to cały urok: funkcja może pamiętać coś między wywołaniami, nie wystawiając zmiennej globalnej, którą mógłby zmienić ktokolwiek. Używaj tego do liczników wywołań, tablic wyszukiwania budowanych przy pierwszym użyciu i flag jednorazowej inicjalizacji.
Ceną jest to, że funkcja przestaje być czystym odwzorowaniem wejścia na wyjście, bo te same argumenty mogą dać różne odpowiedzi. To utrudnia testowanie i sprawia, że wywoływanie jej z kilku wątków naraz bez zabezpieczeń jest niebezpieczne. Używaj lokalnych zmiennych static świadomie, a nie dla wygody.
Zasięg pliku: zmienne globalne
Zmienna zadeklarowana poza wszystkimi funkcjami ma zasięg pliku. Jest widoczna od deklaracji do końca pliku, w każdej funkcji poniżej, a jej czas życia to cały program.
Zmienne globalne są domyślnie inicjalizowane zerem, więc totalOperations zaczyna od 0 nawet bez = 0.
Domyślnie zmienna globalna ma też łączność zewnętrzną (external linkage): inne pliki .c tego samego programu mogą do niej sięgnąć, deklarując ją jako extern:
/* w stats.c */
int totalOperations = 0; /* definicja: dokladnie jedna w programie */
/* w main.c */
extern int totalOperations; /* deklaracja: "istnieje gdzies" */
Umieść deklarację extern w pliku nagłówkowym, żeby każdy użytkownik widział tę samą. Zwróć uwagę na asymetrię: deklaracja extern mówi, że zmienna istnieje, a dokładnie jeden plik .c musi ją faktycznie zdefiniować.
Dlaczego zmienne globalne gryzą
Zmienne globalne to najłatwiejszy sposób na współdzielenie danych i najłatwiejszy sposób na błędy, których nie da się zlokalizować. Cztery konkretne problemy:
Każdy może je zmienić. Gdy totalOperations ma złą wartość, winowajcą może być dowolna linia w dowolnym pliku. Przy parametrze podejrzanymi są miejsca wywołania, które widzisz.
Funkcje przestają być testowalne. Funkcji, która czyta zmienną globalną, nie da się wywołać w izolacji: trzeba najpierw przygotować stan globalny i pamiętać, by go potem zresetować.
Nazwa obowiązuje w całym programie. Zmienna globalna o nazwie count lub buffer prędzej czy później zderzy się z czyjąś inną.
Ich inicjalizatory muszą być stałymi czasu kompilacji. W C zmienną globalną można zainicjalizować tylko wyrażeniem stałym: int limit = readConfig(); się nie skompiluje, podobnie jak int b = a * 2; na poziomie pliku. Wszystko, co wyliczane, trzeba przypisać w czasie działania w jakiejś funkcji konfigurującej, a każdy plik, który dotknie zmiennej globalnej przed tym wywołaniem, zobaczy po cichu 0.
Alternatywy są niemal zawsze dostępne: przekazuj wartości jako parametry, zwracaj wyniki i grupuj powiązany stan w strukturę przekazywaną jawnie. Prawdziwe wyjątki istnieją, na przykład obiekt konfiguracji całego programu albo uchwyt do logowania, ale nawet one lepiej sprawdzają się jako static na poziomie pliku z funkcjami dostępowymi, o czym mowa w następnej sekcji.
static na poziomie pliku: prywatność
Przy zmiennej globalnej lub funkcji static znaczy coś zupełnie innego niż przy zmiennej lokalnej: łączność wewnętrzną (internal linkage). Nazwa staje się prywatna dla swojego pliku .c i nie można do niej sięgnąć z żadnego innego.
/* counter.c */
#include "counter.h"
static int count = 0; /* prywatna dla tego pliku: zaden inny plik jej nie dotknie */
static void validate(void) { /* prywatna funkcja pomocnicza, nie nalezy do interfejsu */
if (count < 0) count = 0;
}
void increment(void) { /* publiczna: zadeklarowana w counter.h */
count++;
validate();
}
int getCount(void) { /* publiczna */
return count;
}
Inny plik może wywołać increment i getCount, ale nie widzi count i nie może wywołać validate, bo linker nie rozwiąże tych nazw. To system modułów w C, na ile w ogóle go ma: nagłówek deklarujący funkcje publiczne i static na wszystkim innym.
Poza porządkiem są dwie praktyczne korzyści. Dwa pliki mogą mieć każdy własne static void validate(void) bez kolizji, czego nie mogłyby, gdyby funkcje były publiczne. A kompilator wie, że funkcja static nie ma wywołań spoza pliku, więc może ją agresywniej rozwijać inline albo ostrzec, że jest nieużywana.
Słowo kluczowe ma więc dwa odrębne znaczenia, a decyduje o nich to, gdzie się pojawia:
static przy zmiennej LOKALNEJ -> czas zycia staje sie staly (zasieg bez zmian)
static na poziomie PLIKU -> lacznosc staje sie wewnetrzna (czas zycia bez zmian)
Przesłanianie
Zadeklarowanie w zasięgu wewnętrznym nazwy, która już istnieje w zewnętrznym, przesłania zewnętrzną nazwę: w zasięgu wewnętrznym nazwa odnosi się do nowej zmiennej, a zewnętrzna jest nieosiągalna.
To jest dozwolone i czasem zamierzone. Częściej jednak to wypadek, i to kosztowny: funkcja, która miała zaktualizować zmienną globalną, aktualizuje zmienną lokalną o tej samej nazwie, a globalna nigdy się nie zmienia. To samo dzieje się, gdy zmienna lokalna przesłania parametr, więc przypisanie przeznaczone dla wejścia trafia donikąd.
gcc -Wshadow zgłasza każdy taki przypadek. Nie wchodzi w skład -Wall, więc włącz go jawnie:
gcc -Wall -Wextra -Wshadow program.c -o program
Nawyk, który całkowicie eliminuje ten problem, to nazywanie według roli, a nie typu: totalScore i itemScore nie mogą się wzajemnie przesłonić, a dwie zmienne o nazwie n w końcu to zrobią.
Ściąga
zadeklarowana w bloku zasieg blokowy, zycie automatyczne, smieci bez inicjalizacji
parametr funkcji zasieg ciala funkcji, kopia argumentu
static wewnatrz funkcji zasieg blokowy, zycie CALEGO PROGRAMU, zero na start, zachowana miedzy wywolaniami
zadeklarowana poza funkcjami zasieg pliku, zycie programu, zero na start, widoczna dla innych plikow
static poza funkcjami zasieg pliku, zycie programu, PRYWATNA dla tego pliku .c
deklaracja extern nazywa zmienna zdefiniowana w innym pliku
Najczęściej zadawane pytania
Czym jest zasięg w C?
To obszar kodu, w którym nazwa jest widoczna. Zmienna zadeklarowana wewnątrz bloku (pary nawiasów klamrowych) jest widoczna tylko w tym bloku: to zasięg blokowy. Zmienna zadeklarowana poza wszystkimi funkcjami ma zasięg pliku i jest widoczna od deklaracji do końca pliku.
Co robi static ze zmienną lokalną w C?
Zmienia czas życia zmiennej, nie zmieniając jej zasięgu. Lokalna zmienna static jest tworzona raz, inicjalizowana raz i zachowuje wartość między wywołaniami, ale nadal jest widoczna tylko wewnątrz swojej funkcji. To sposób, by dać funkcji pamięć bez używania zmiennej globalnej.
Czym różni się static od zmiennej globalnej w C?
Zwykła zmienna globalna jest widoczna dla każdego pliku programu, a inne pliki mogą do niej sięgnąć przez extern. Zmienna lub funkcja static o zasięgu pliku jest prywatna dla jednego pliku .c i inne pliki nie mogą się z nią zlinkować. static na poziomie pliku dotyczy ukrywania, a static przy zmiennej lokalnej dotyczy czasu życia.
Dlaczego zmienne globalne w C są złe?
Każda funkcja może je zmienić, więc błąd może pochodzić z dowolnego miejsca w programie. Funkcji, które je czytają, nie da się testować w izolacji, a nazwa zajmuje cały program i prowokuje kolizje. Zamiast tego przekazuj wartości jako parametry i zwracaj wyniki, a tam, gdzie wspólny stan jest naprawdę potrzebny, zrób go static na poziomie pliku, aby dotykać go mógł tylko jeden plik.