Wskaźnik zawsze przechowuje jakąś wartość. Gdy nie ma jeszcze obiektu, na który mógłby wskazywać (przed przypisaniem, po zwolnieniu tego, na co wskazywał, albo gdy funkcja nie ma czego zwrócić), potrzebuje wartości, która jednoznacznie oznacza "tu nic nie ma". Tą wartością jest wskaźnik pusty, a NULL to nazwa, którą piszesz.
Związana z nim dyscyplina jest niewielka i opłaca się bez przerwy: ustawiaj wskaźniki na NULL, gdy nie mają celu, sprawdzaj NULL przed wyłuskaniem i zwracaj NULL, gdy nie masz wyniku.
Czym naprawdę jest NULL
NULL to makro zdefiniowane w <stddef.h> i dołączane też przez <stdio.h>, <stdlib.h>, <string.h> i inne nagłówki. Rozwija się do stałej wskaźnika pustego, zwykle ((void*)0) albo zwykłego 0.
Standard C gwarantuje tę właściwość, która ma znaczenie: wskaźnik pusty jest różny od wskaźnika na jakikolwiek prawdziwy obiekt albo funkcję. Dwa wskaźniki puste tego samego typu są zawsze sobie równe.
Większość systemów wypisuje dla wskaźnika pustego (nil) albo 0x0, czyli adres zero. To częsta implementacja, a nie reguła; standard nigdy nie obiecuje, że bity są zerami. Obiecuje porównanie, więc pisz p == NULL, a nigdy nic, co zależy od reprezentacji.
Wyłuskanie NULL kończy się awarią
Oto powód istnienia każdego sprawdzenia NULL:
int *p = NULL;
printf("%d\n", *p); // niezdefiniowane zachowanie: prawie na pewno awaria
Na Windows, macOS i Linuksie pierwsza strona przestrzeni adresowej celowo nie jest zmapowana, więc procesor przechwytuje dostęp, a system operacyjny zabija program. Zobaczysz:
Segmentation fault (core dumped) # Linux
zsh: segmentation fault ./program # macOS
Ta natychmiastowa awaria to zaleta. Zamienia błąd logiczny w głośną porażkę dokładnie w linii, która go spowodowała, zamiast cichego uszkodzenia danych, które znajdziesz trzy tygodnie później. Jak czytać taką awarię i znaleźć linię, opisuje strona o błędzie segmentacji.
Dlatego "null pointer" nie jest w C wyjątkiem, który się łapie. Nie ma mechanizmu odzyskiwania; jest tylko sprawdzanie zawczasu.
Sprawdzaj to, co może zawieść
Funkcje, które mogą się nie powieść, sygnalizują to, zwracając NULL. Sprawdzanie nie jest opcjonalną uprzejmością: alternatywą jest awaria.
Biblioteka standardowa jest ich pełna: malloc, calloc, realloc, fopen, strchr, strstr, getenv, bsearch. Każda z nich odpowiada na "brak wyniku" przez NULL. Całą historię przydzielania pamięci opisuje strona o pamięci dynamicznej.
Dwa idiomy sprawdzenia, oba popularne:
if (p == NULL) { /* obsłuż */ } // jawnie: lepsze dla czytelności
if (!p) { /* obsłuż */ } // zwięźle: zakłada, że NULL jest fałszem
Wskaźnik pusty w warunku jest fałszem, a poprawny wskaźnik prawdą, więc oba działają. Forma jawna lepiej się czyta, gdy obca osoba przegląda twój kod w poszukiwaniu błędów.
NULL a 0 a niezainicjalizowany wskaźnik
Trzy różne sytuacje, które początkujący mieszają ze sobą:
int *a = NULL; "wskazuje na nic": da się sprawdzić, bezpieczny test
int *b; niezainicjalizowany: zawiera śmieci, NIE da się sprawdzić
int *c = &x; wskazuje na prawdziwy obiekt
Groźny jest b. Jego bajty to to, co zostało w tym miejscu stosu, a może to być zero (które wygląda dobrze) albo wiarygodnie wyglądający adres z wcześniejszego wywołania. Żadne sprawdzenie nie odróżni go od poprawnego wskaźnika, a zachowanie zmienia się między buildem debug i release.
Inicjalizuj każdy wskaźnik. Jeśli nie masz jeszcze adresu, adresem jest NULL. Kompiluj z -Wall -Wextra, a kompilator sam zgłosi wiele użyć niezainicjalizowanych zmiennych.
O 0: w kontekście wskaźnika stała całkowita 0 jest stałą wskaźnika pustego, więc p = 0; to poprawne C. Mimo to wybieraj NULL. Sygnalizuje intencję i ma znaczenie w wywołaniach ze zmienną liczbą argumentów, gdzie kompilator nie może dokonać konwersji za ciebie:
execl("/bin/ls", "ls", 0); // ryzykowne: może przekazać int tam, gdzie oczekiwany jest wskaźnik
execl("/bin/ls", "ls", (char *)NULL); // poprawnie
C23 dodaje nullptr, słowo kluczowe z własnym typem, które całkowicie usuwa ten rodzaj niejednoznaczności; NULL pozostaje poprawne i przenośne wszędzie.
Wzorce obronne
Zabezpieczenie na początku funkcji, która przyjmuje wskaźniki.
Po zwolnieniu ustaw NULL. free nie zmienia twojego wskaźnika, tylko zwalnia pamięć. Nieaktualny wskaźnik, który zostaje, to wskaźnik wiszący, a jego użycie to niezdefiniowane zachowanie, które często nie kończy się od razu awarią.
Dwa fakty z tego przykładu warto zapamiętać. free(NULL) z definicji nic nie robi, więc kod sprzątający nigdy nie musi go zabezpieczać. A ustawienie p = NULL po free zamienia użycie po zwolnieniu, które może po cichu uszkodzić dane, w natychmiastową awarię, którą da się zdebugować.
Zwracaj NULL jako "brak wyniku" i mów o tym.
// zwraca wskaźnik na pasujący element albo NULL, jeśli go nie ma
int *find(int *arr, int n, int target);
Udokumentuj to w komentarzu nad funkcją. Wywołujący, który wie, że możliwe jest NULL, napisze sprawdzenie; ten, który nie wie, nie napisze.
Szybka lista kontrolna
- Inicjalizuj każdy wskaźnik prawdziwym adresem albo wartością
NULL. - Sprawdzaj wynik
malloc,calloc,reallocifopenprzed użyciem. - Sprawdzaj parametry wskaźnikowe na początku każdej funkcji, którą można wywołać ze złymi danymi.
- Ustawiaj wskaźniki na
NULLod razu pofree. - Pisz
NULL, a nie0, zawsze, gdy chodzi o wskaźnik. - Buduj z
-Wall -Wextra, a gdy możesz, uruchamiaj z sanitizerem:gcc -fsanitize=address,undefinedwyłapuje wyłuskania NULL z dokładnym raportem.
Najczęściej zadawane pytania
Czym jest wskaźnik NULL w C?
Wskaźnikiem przechowującym stałą wskaźnika pustego, czyli wartość, która na pewno jest różna od adresu każdego prawdziwego obiektu. To standardowy sposób, żeby powiedzieć "ten wskaźnik jeszcze na nic nie wskazuje", a NULL to makro dla tej wartości, zdefiniowane w <stddef.h> i kilku innych nagłówkach.
Co się dzieje po wyłuskaniu wskaźnika NULL w C?
To niezdefiniowane zachowanie. W praktyce na komputerach i serwerach program natychmiast się wysypuje z błędem segmentacji, bo adres zero celowo nie jest zmapowany. Na systemach wbudowanych bez ochrony pamięci może zamiast tego po cichu coś odczytać albo uszkodzić, co jest dużo gorsze.
Czy NULL to to samo co 0 w C?
W kontekście wskaźników tak: stała całkowita 0 jest stałą wskaźnika pustego, więc działa zarówno p = 0;, jak i p == NULL. Mimo to używaj NULL dla wskaźników, a 0 dla liczb: czytelnik wie wtedy, o co ci chodziło, a wywołania ze zmienną liczbą argumentów pozostają bezpieczne, bo samo 0 może zostać przekazane jako int, a nie wskaźnik.
Czym różni się wskaźnik NULL od niezainicjalizowanego wskaźnika?
Wskaźnik NULL na pewno nie wskazuje na nic i można to sprawdzić. Niezainicjalizowany wskaźnik przechowuje bajty, które akurat były w tej pamięci, być może adres wyglądający na poprawny, więc żaden test go nie wyłapie. Gdy nie masz nic lepszego, zawsze inicjalizuj wskaźniki wartością NULL.