Kiedy wskaźniki powinny być sprawdzane pod kątem NULL w C?


18

Podsumowanie :

Czy funkcja w C zawsze powinna sprawdzać, aby upewnić się, że nie usuwa dereferencji ze NULLwskaźnika? Jeśli nie, kiedy należy pominąć te kontrole?

Szczegóły :

Czytałem kilka książek o programowaniu wywiadów i zastanawiam się, jaki jest odpowiedni stopień sprawdzania poprawności danych wejściowych dla argumentów funkcji w C? Oczywiście każda funkcja, która pobiera dane wejściowe od użytkownika, musi przeprowadzić walidację, w tym sprawdzić NULLwskaźnik przed usunięciem go z listy. Ale co w przypadku funkcji w tym samym pliku, której nie spodziewasz się ujawnić za pośrednictwem interfejsu API?

Na przykład następujący pojawia się w kodzie źródłowym git:

static unsigned short graph_get_current_column_color(const struct git_graph *graph)
{
    if (!want_color(graph->revs->diffopt.use_color))
        return column_colors_max;
    return graph->default_column_color;
}

Jeśli *graphjest, NULLwówczas zerowy wskaźnik zostanie usunięty z dereferencji, prawdopodobnie zawieszając program, ale prawdopodobnie powodując inne nieprzewidziane zachowanie. Z drugiej strony funkcja jest, staticwięc może programista już zatwierdził dane wejściowe. Nie wiem, wybrałem go losowo, ponieważ był to krótki przykład w aplikacji napisanej w C. Widziałem wiele innych miejsc, w których używane są wskaźniki bez sprawdzania wartości NULL. Moje pytanie nie jest ogólne dla tego segmentu kodu.

Widziałem podobne pytanie zadawane w kontekście przekazywania wyjątków . Jednak w przypadku niebezpiecznego języka, takiego jak C lub C ++, nie występuje automatyczne propagowanie błędów nieobsługiwanych wyjątków.

Z drugiej strony widziałem dużo kodu w projektach typu open source (takich jak powyższy przykład), które nie sprawdzają wskaźników przed ich użyciem. Zastanawiam się, czy ktoś ma przemyślenia na temat tego, kiedy należy sprawdzać funkcję, czy zakładać, że funkcja została wywołana z poprawnymi argumentami.

Ogólnie interesuje mnie to pytanie dotyczące pisania kodu produkcyjnego. Ale interesuję się również w kontekście wywiadów programistycznych. Na przykład wiele podręczników algorytmów (takich jak CLR) ma tendencję do przedstawiania algorytmów w pseudokodzie bez sprawdzania błędów. Jednak chociaż jest to dobre dla zrozumienia rdzenia algorytmu, to oczywiście nie jest dobrą praktyką programowania. Nie chciałbym więc mówić ankieterowi, że pomijam sprawdzanie błędów, aby uprościć przykłady kodu (tak jak w podręczniku). Ale nie chciałbym też wydawać się, że produkuje nieefektywny kod z nadmierną kontrolą błędów. Na przykład graph_get_current_column_colormożna go zmodyfikować, aby sprawdzał, czy ma *graphwartość NULL, ale nie jest jasne, co by zrobił, gdyby *graphmiał wartość NULL, inaczej niż nie powinien go wyłapywać.


7
Jeśli piszesz funkcję dla interfejsu API, w której dzwoniący nie powinni rozumieć wewnętrznych elementów, jest to jedno z tych miejsc, w których dokumentacja jest ważna. Jeśli udokumentujesz, że argument musi być prawidłowym wskaźnikiem innym niż NULL, sprawdzenie go staje się obowiązkiem osoby dzwoniącej.
Blrfl


Z perspektywy roku 2017, pamiętając o pytaniu i większości odpowiedzi, które zostały napisane w 2013 roku, czy którakolwiek z odpowiedzi odnosi się do problemu niezdefiniowanych zachowań w czasie w związku z optymalizacją kompilatorów?
rwong

W przypadku wywołań API oczekujących poprawnych argumentów wskaźnika zastanawiam się, jaka jest wartość testowania tylko dla wartości NULL? Każdy nieprawidłowy wskaźnik, który zostanie zdereferencjonowany, byłby tak samo zły jak NULL i mimo to segfault.
PaulHK

Odpowiedzi:


15

Nieprawidłowe wskaźniki zerowe mogą być spowodowane błędem programisty lub błędem środowiska wykonawczego. Błędy w czasie wykonywania są czymś, czego programista nie może naprawić, na przykład mallocawarią spowodowaną brakiem pamięci, siecią upuszczającą pakiet lub wprowadzaniem czegoś głupiego przez użytkownika. Błędy programatora są spowodowane przez programistę niepoprawnie używającą tej funkcji.

Ogólna zasada, którą widziałem, polega na tym, że błędy czasu wykonywania powinny być zawsze sprawdzane, ale błędy programisty nie muszą być sprawdzane za każdym razem. Powiedzmy, że jakiś programista-idiota zadzwonił bezpośrednio graph_get_current_column_color(0). Oddzieli się po pierwszym wywołaniu, ale po naprawieniu poprawka jest kompilowana na stałe. Nie trzeba sprawdzać za każdym razem, gdy jest uruchamiany.

Czasami, szczególnie w bibliotekach stron trzecich, assertzamiast ifinstrukcji zobaczysz komunikat o sprawdzeniu błędów programisty . Pozwala to na skompilowanie kontroli podczas programowania i pominięcie ich w kodzie produkcyjnym. Od czasu do czasu widziałem również bezpłatne kontrole, w których źródło potencjalnego błędu programisty jest dalekie od symptomu.

Oczywiście, zawsze możesz znaleźć kogoś bardziej pedantycznego, ale większość programistów C, których znam, preferuje mniej zaśmiecony kod niż kod, który jest marginalnie bezpieczniejszy. „Bezpieczniejszy” to subiektywny termin. Rażący segfault podczas programowania jest lepszy niż subtelny błąd korupcji w terenie.


Pytanie jest nieco subiektywne, ale na razie wydaje się najlepszą odpowiedzią. Dziękujemy wszystkim, którzy podzielili się przemyśleniami na ten temat.
Gabriel Southern

1
W iOS malloc nigdy nie zwróci NULL. Jeśli nie znajdzie pamięci, najpierw poprosi aplikację o zwolnienie pamięci, następnie poprosi system operacyjny (który poprosi inne aplikacje o zwolnienie pamięci i ewentualnie ich zabicie), a jeśli nadal nie ma pamięci, zabije twoją aplikację . Czeki nie są potrzebne.
gnasher729

11

Kernighan & Plauger w „Narzędziach programowych” napisał, że sprawdzą wszystko, a dla warunków, które ich zdaniem mogą się nigdy nie wydarzyć, przerwie się z komunikatem o błędzie „Nie może się zdarzyć”.

Mówią, że są bardzo upokorzeni liczbą wyświetleń „Nie mogą się zdarzyć” na swoich terminalach.

ZAWSZE powinieneś sprawdzać, czy wskaźnik nie ma wartości NULL, zanim (próbujesz) wyrejestrować go. ZAWSZE . Ilość kodu, który duplikujesz, sprawdzając, czy wartości NULL się nie zdarzają, a procesor „marnuje” cykle, będzie więcej niż opłacona przez liczbę awarii, których nie musisz debugować z niczego poza zrzutem awaryjnym - jeśli masz szczęście.

Jeśli wskaźnik jest niezmienny w pętli, wystarczy sprawdzić go poza pętlą, ale należy go „skopiować” do zmiennej lokalnej o ograniczonym zakresie, do użycia przez pętlę, która dodaje odpowiednie dekoracje const. W takim przypadku MUSISZ upewnić się, że każda funkcja wywoływana z korpusu pętli zawiera niezbędne ozdoby const na prototypach, WSZYSTKO W DÓŁ. Jeśli nie, to czy nie może (z powodu np dostawcy pakietu lub współpracownika upartego), trzeba sprawdzić, czy nie NULL za każdym razem może to być modyfikowane , ponieważ pewne jak COL Murphy był niepoprawnym optymistą, ktoś JEST dzieje załamać, kiedy nie patrzysz.

Jeśli znajdujesz się w funkcji, a wskaźnik nie ma wartości NULL, powinieneś go zweryfikować.

Jeśli otrzymujesz go z funkcji, która nie ma wartości NULL, powinieneś ją zweryfikować. Malloc () jest szczególnie znany z tego powodu. (Nortel Networks, teraz nieczynne, miał na ten temat twardy i szybki standard kodowania. W pewnym momencie udało mi się debugować awarię, którą przywróciłem do malloc () zwracając wskaźnik NULL i koder idiota nie zawracał sobie głowy sprawdzaniem zanim napisał do niego, ponieważ po prostu WIEDZIAŁ, że ma mnóstwo pamięci ... Powiedziałem kilka bardzo nieprzyjemnych rzeczy, kiedy w końcu znalazłem.)


8
Jeśli korzystasz z funkcji, która wymaga wskaźnika innego niż NULL, ale mimo to sprawdzasz i jest NULL ... co dalej?
detly

1
@detly albo przestań, co robisz, i zwróć kod błędu, albo potknij się o potwierdzenie
James

1
@James - o tym nie pomyślałem assert. Nie podoba mi się pomysł na kod błędu, jeśli mówisz o zmianie istniejącego kodu, aby uwzględnić NULLkontrole.
detly

10
@detly, nie dostaniesz się tak daleko jak C dev, jeśli nie lubisz kodów błędów
James

5
@ JohnR.Strohm - to jest C, to twierdzenia lub nic: P
detly

5

Możesz pominąć zaznaczenie, kiedy możesz się w jakiś sposób przekonać, że wskaźnik nie może być zerowy.

Zwykle sprawdzanie wskaźnika zerowego jest realizowane w kodzie, w którym oczekuje się, że null pojawi się jako wskaźnik, że obiekt jest obecnie niedostępny. Wartość Null jest używana jako wartość wartownika, na przykład do zakończenia połączonych list, a nawet tablic wskaźników. argvWektor ciągów przekazywanych do mainma obowiązek być zakończony zerem przez wskaźnik, podobnie jak łańcuch jest zakończony znakiem NULL: argv[argc]jest wskaźnik null, można liczyć na to podczas analizowania wiersza polecenia.

while (*argv) {
   /* process argument string *argv */
   argv++; /* increment to next one */
}

Tak więc sytuacje sprawdzania wartości null to takie, w których a jest wartością oczekiwaną. Kontrole zerowe implementują znaczenie wskaźnika zerowego, takie jak zatrzymanie wyszukiwania listy połączonej. Zapobiegają one dereferencjowaniu wskaźnika przez kod.

W sytuacji, w której projekt nie oczekuje wartości wskaźnika zerowego, nie ma sensu jej sprawdzać. Jeśli pojawi się niepoprawna wartość wskaźnika, najprawdopodobniej będzie wyglądać na inną niż null, której nie można odróżnić od prawidłowych wartości w żaden przenośny sposób. Na przykład wartość wskaźnika uzyskana z odczytu niezainicjowanej pamięci interpretowanej jako typ wskaźnika, wskaźnik uzyskany przez jakąś podejrzaną konwersję lub wskaźnik zwiększony poza granice.

O typie danych, takim jak graph *: można to zaprojektować tak, aby wartość null była prawidłowym wykresem: coś bez krawędzi i bez węzłów. W takim przypadku wszystkie funkcje przyjmujące graph *wskaźnik będą musiały poradzić sobie z tą wartością, ponieważ jest to poprawna wartość domeny w reprezentacji grafów. Z drugiej strony a graph *może być wskaźnikiem do obiektu podobnego do kontenera, który nigdy nie jest zerowy, jeśli trzymamy wykres; wskaźnik zerowy może wtedy powiedzieć nam, że „obiekt wykresu nie jest obecny; jeszcze go nie przydzieliliśmy lub uwolniliśmy; lub ten wykres nie jest obecnie powiązany”. To ostatnie użycie wskaźników jest połączoną wartością logiczną / satelitarną: wskaźnik, który nie jest pusty, wskazuje „Mam ten siostrzany obiekt” i zapewnia ten obiekt.

Możemy ustawić wskaźnik na zero, nawet jeśli nie zwalniamy obiektu, aby po prostu oddzielić jeden obiekt od drugiego:

tty_driver->tty = NULL; /* detach low level driver from the tty device */

Najbardziej przekonującym argumentem, jaki znam, że wskaźnik nie może być pusty w pewnym momencie, jest zawinięcie tego punktu w „if (ptr! = NULL) {” i odpowiadające mu „}”. Poza tym jesteś na terytorium formalnej weryfikacji.
John R. Strohm

4

Pozwól, że dodam jeszcze jeden głos do fugi.

Podobnie jak wiele innych odpowiedzi, mówię - nie zawracaj sobie głowy sprawdzaniem w tym momencie; to obowiązek osoby dzwoniącej. Ale mam podstawę do budowania raczej niż prostą praktyczność (i arogancję programowania C).

Staram się podążać za zasadą Donalda Knutha, aby programy były jak najbardziej kruche. Jeśli coś pójdzie nie tak, spowoduj duże awarie , a odwołanie się do wskaźnika zerowego jest zwykle dobrym sposobem na zrobienie tego. Ogólna idea jest taka, że ​​awaria lub nieskończona pętla jest o wiele lepsza niż tworzenie niewłaściwych danych. I przyciąga uwagę programistów!

Jednak odwołanie się do wskaźników zerowych (szczególnie w przypadku dużych struktur danych) nie zawsze powoduje awarię. Westchnienie. To prawda. I tam właśnie wpadają Asserty. Są proste, mogą natychmiast zawiesić Twój program (który odpowiada na pytanie: „Co powinna zrobić metoda, jeśli napotka zero”) i mogą być włączane / wyłączane w różnych sytuacjach (zalecam NIE wyłączając ich, ponieważ lepiej jest, aby klienci mieli awarię i zobaczyli zaszyfrowaną wiadomość niż złe dane).

To moje dwa centy.


1

Zasadniczo sprawdzam tylko, kiedy wskaźnik jest przypisany, co jest na ogół jedynym czasem, kiedy mogę coś z tym zrobić i ewentualnie odzyskać, jeśli jest nieprawidłowy.

Jeśli na przykład dostanę uchwyt do okna, sprawdzę, czy ma ono wartość null, i wtedy i tam, i zrobię coś z warunkiem null, ale nie będę sprawdzać, czy ma ono wartość null za każdym razem Używam wskaźnika, w każdej funkcji, do której wskaźnik jest przekazywany, w przeciwnym razie miałbym góry duplikatów kodu obsługi błędów.

Funkcje takie jak graph_get_current_column_colorprawdopodobnie nie są w stanie zrobić nic użytecznego w twojej sytuacji, jeśli napotka zły wskaźnik, więc zostawiłbym sprawdzanie NULL dla swoich rozmówców.


1

Powiedziałbym, że zależy to od następujących kwestii:

  1. Czy wykorzystanie procesora ma krytyczne znaczenie? Każde sprawdzenie NULL zajmuje trochę czasu.
  2. Jakie są szanse, że wskaźnik ma wartość NULL? Czy został użyty tylko w poprzedniej funkcji. Czy można zmienić wartość wskaźnika?
  3. Czy system ma charakter zapobiegawczy? Czy może nastąpić zmiana zadania i zmiana wartości? Czy ISR może wejść i zmienić wartość?
  4. Jak ściśle powiązany jest kod?
  5. Czy istnieje jakiś automatyczny mechanizm, który automatycznie sprawdza wskaźniki NULL automatycznie?

Wskaźnik wykorzystania procesora / kursów ma wartość NULL Za każdym razem, gdy sprawdzasz wartość NULL, zajmuje to trochę czasu. Z tego powodu staram się ograniczyć kontrole do miejsca, w którym wskaźnik mógł zostać zmieniony.

System wyprzedzający Jeśli kod działa, a inne zadanie może go przerwać i potencjalnie zmienić wartość, warto sprawdzić.

Ściśle połączone moduły Jeśli system jest ściśle połączony, wówczas sensowne jest, aby mieć więcej kontroli. Rozumiem przez to, że jeśli struktury danych są współużytkowane przez wiele modułów, jeden moduł może coś zmienić spod innego modułu. W takich sytuacjach warto sprawdzać częściej.

Automatyczne kontrole / pomoc sprzętowa Ostatnią rzeczą, którą należy wziąć pod uwagę, jest to, czy sprzęt, na którym pracujesz, ma jakiś mechanizm, który może sprawdzić, czy NULL. W szczególności mam na myśli wykrywanie błędów strony. Jeśli w systemie jest wykrywanie błędów stron, sam procesor może sprawdzić dostęp NULL. Osobiście uważam, że jest to najlepszy mechanizm, ponieważ zawsze działa i nie polega na tym, że programiści przeprowadzają jawne kontrole. Ma również tę zaletę, że praktycznie zerowy narzut. Jeśli jest dostępny, polecam go, debugowanie jest trochę trudniejsze, ale nie przesadnie.

Aby sprawdzić, czy jest dostępny, utwórz program ze wskaźnikiem. Ustaw wskaźnik na 0, a następnie spróbuj go odczytać / zapisać.


Nie wiem, czy sklasyfikowałbym segfault jako automatyczne sprawdzanie NULL. Zgadzam się, że posiadanie ochrony pamięci procesora pomaga, aby jeden proces nie wyrządził tyle szkody reszcie systemu, ale nie nazwałbym tego automatyczną ochroną.
Gabriel Southern

1

Moim zdaniem sprawdzanie poprawności danych wejściowych (warunki wstępne / końcowe, tj.) Dobrze jest wykrywać błędy programowania, ale tylko wtedy, gdy powoduje głośne i wstrętne błędy zatrzymania pokazu, których nie można zignorować. assertzazwyczaj ma taki efekt.

Wszystko, czego się nie uda, może przerodzić się w koszmar bez bardzo starannie koordynowanych zespołów. I oczywiście idealnie wszystkie zespoły są bardzo dokładnie skoordynowane i zjednoczone zgodnie z surowymi standardami, ale większość środowisk, w których pracowałem, była o wiele za słaba.

Jako przykład pracowałem z kilkoma kolegami, którzy wierzyli, że należy religijnie sprawdzić obecność zerowych wskaźników, więc posypali dużo kodu w ten sposób:

void vertex_move(Vertex* v)
{
     if (!v)
          return;
     ...
}

... a czasami po prostu tak, nawet bez zwracania / ustawiania kodu błędu. Było to w bazie kodu, która miała kilka dziesięcioleci z wieloma nabytymi wtyczkami stron trzecich. Była to także baza kodów nękana wieloma błędami i często błędami, które były bardzo trudne do wyśledzenia z przyczyn źródłowych, ponieważ miały one tendencję do zawieszania się w witrynach odległych od bezpośredniego źródła problemu.

I ta praktyka była jednym z powodów. Jest to naruszenie ustalonego warunku wstępnego powyższej move_vertexfunkcji, aby przekazać do niej wierzchołek zerowy, ale taka funkcja po prostu po cichu przyjęła ją i nic nie zrobiła w odpowiedzi. Tak więc zdarzało się, że wtyczka mogła mieć błąd programisty, który powoduje, że przekazuje zerową wartość do wspomnianej funkcji, tylko nie wykrywa jej, tylko robi wiele rzeczy później, a ostatecznie system zaczyna się wyładowywać lub ulega awarii.

Ale prawdziwym problemem była tutaj niemożność łatwego wykrycia tego problemu. Kiedyś próbowałem zobaczyć, co by się stało, gdybym zamienił powyższy kod analogiczny na assert:

void vertex_move(Vertex* v)
{
     assert(v && "Vertex should never be null!");
     ...
}

... i ku mojemu przerażeniu stwierdziłem, że twierdzenie to nie działa w lewo ani w prawo, nawet po uruchomieniu aplikacji. Po tym, jak naprawiłem kilka pierwszych stron z wezwaniami, zrobiłem kilka rzeczy, a potem dostałem więcej błędów asercji. Kontynuowałem, dopóki nie zmodyfikowałem tak dużo kodu, że w końcu cofnąłem moje zmiany, ponieważ stały się zbyt natrętne i niechętnie zachowywały kontrolę zerowego wskaźnika, zamiast tego dokumentując, że funkcja pozwala zaakceptować zerowy wierzchołek.

Ale to niebezpieczeństwo, choć w najgorszym przypadku, polegające na tym, że nie można łatwo wykryć naruszenia warunków wstępnych / następczych. Następnie możesz z biegiem lat cicho gromadzić ładunek kodu naruszający takie warunki wstępne / końcowe podczas lotu pod radarem testowania. Moim zdaniem taki zerowy wskaźnik sprawdza poza rażącym i wstrętnym niepowodzeniem asercji może w rzeczywistości wyrządzić o wiele więcej szkody niż pożytku.

Jeśli chodzi o zasadnicze pytanie, kiedy powinieneś sprawdzić zerowe wskaźniki, wierzę w swobodne stwierdzanie, czy ma ono na celu wykrycie błędu programisty, i nie pozwalanie, aby milczało i było trudne do wykrycia. Jeśli nie jest to błąd programowania i coś poza kontrolą programisty, np. Awaria braku pamięci, warto sprawdzić, czy nie występuje błąd i użyć obsługi błędów. Poza tym jest to pytanie projektowe oparte na tym, co twoje funkcje uznają za prawidłowe warunki przed / po.


0

Jedną praktyką jest zawsze przeprowadzanie kontroli zerowej, chyba że już ją sprawdziłeś; więc jeśli dane wejściowe są przekazywane z funkcji A () do B (), a A () już zweryfikował wskaźnik i masz pewność, że B () nie jest wywoływany nigdzie indziej, to B () może zaufać A (), że ma zdezynfekowane dane.


1
... aż za 6 miesięcy ktoś przyjdzie i doda trochę kodu, który wywołuje B () (prawdopodobnie zakładając, że ktokolwiek napisał B () z pewnością sprawdził poprawnie wartości NULL). Więc jesteś pieprzony, prawda? Podstawowa zasada - jeśli istnieje niepoprawny warunek dla wejścia do funkcji, sprawdź go, ponieważ dane wejściowe są poza kontrolą funkcji.
Maximus Minimus

@ mh01 Jeśli po prostu rozbijasz losowy kod (tj. robisz założenia i nie czytasz dokumentacji), to nie sądzę, żeby dodatkowe NULLkontrole zrobiły wiele. Pomyśl o tym: teraz B()sprawdza NULLi ... co robi? Powrócić -1? Jeśli dzwoniący nie sprawdzi NULL, czy możesz mieć pewność, że i tak zajmie się sprawą -1wartości zwrotu?
detly

1
To odpowiedzialność za osoby dzwoniące. Radzisz sobie z własną odpowiedzialnością, która obejmuje nieufność do jakichkolwiek arbitralnych / niepoznawalnych / potencjalnie niezweryfikowanych danych wejściowych. W przeciwnym razie jesteś w mieście typu cop-out. Jeśli dzwoniący nie sprawdzi, wtedy dzwoniący spieprzył; sprawdziłeś, twój tyłek jest objęty ubezpieczeniem, możesz powiedzieć każdemu, kto napisał do dzwoniącego, że przynajmniej zrobiłeś dobrze.
Maximus Minimus
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.