Podsumowanie :
Czy funkcja w C zawsze powinna sprawdzać, aby upewnić się, że nie usuwa dereferencji ze NULL
wskaź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ć NULL
wskaź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 *graph
jest, NULL
wó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, static
wię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_color
można go zmodyfikować, aby sprawdzał, czy ma *graph
wartość NULL, ale nie jest jasne, co by zrobił, gdyby *graph
miał wartość NULL, inaczej niż nie powinien go wyłapywać.
źródło
Odpowiedzi:
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
malloc
awarią 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,
assert
zamiastif
instrukcji 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.
źródło
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.)
źródło
assert
. Nie podoba mi się pomysł na kod błędu, jeśli mówisz o zmianie istniejącego kodu, aby uwzględnićNULL
kontrole.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.
argv
Wektor ciągów przekazywanych domain
ma 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.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ącegraph *
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 agraph *
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:
źródło
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.
źródło
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_color
prawdopodobnie 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.źródło
Powiedziałbym, że zależy to od następujących kwestii:
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ć.
źródło
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ć.
assert
zazwyczaj 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:
... 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_vertex
funkcji, 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
:... 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.
źródło
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.
źródło
NULL
kontrole zrobiły wiele. Pomyśl o tym: terazB()
sprawdzaNULL
i ... co robi? Powrócić-1
? Jeśli dzwoniący nie sprawdziNULL
, czy możesz mieć pewność, że i tak zajmie się sprawą-1
wartości zwrotu?