Co to znaczy wykonać “ zerowe sprawdzenie ” w C lub C ++?

Uczyłem się C ++ i ciężko mi zrozumieć null. W szczególności samouczki, które przeczytałem, wspominają o wykonaniu „sprawdzenia zerowego”, ale nie jestem pewien, co to oznacza i dlaczego jest to konieczne.

  • Co dokładnie jest zerowe?
  • Co to znaczy „sprawdzić wartość null”?
  • Czy zawsze muszę sprawdzać wartość null?

Wszelkie przykłady kodu byłyby bardzo mile widziane.

Komentarze

Odpowiedź

W C i C ++ wskaźniki są z natury niebezpieczne, to znaczy, gdy wyłuskujemy wskaźnik, Twoim obowiązkiem jest upewnienie się, że wskazuje gdzieś prawidłowe; jest to częścią tego, o co chodzi w „ręcznym zarządzaniu pamięcią” (w przeciwieństwie do schematów automatycznego zarządzania pamięcią zaimplementowanych w językach takich jak Java , PHP lub .NET, które nie pozwalają na tworzenie nieprawidłowych referencji bez większego wysiłku).

Typowym rozwiązaniem, które wyłapuje wiele błędów, jest ustawienie wszystkich wskaźników, które na nic nie wskazują jako NULL (lub, w poprawnym C ++, 0) i sprawdzając to przed uzyskaniem dostępu do wskaźnika. W szczególności powszechną praktyką jest inicjowanie wszystkich wskaźników na NULL (chyba że masz już coś, na co je wskażesz, kiedy je deklarujesz) i ustawianie ich na NULL, gdy delete lub free() je (chyba że zaraz po tym wyjdą poza zakres). Przykład (w C, ale poprawny C ++):

void fill_foo(int* foo) { *foo = 23; // this will crash and burn if foo is NULL } 

Lepsza wersja:

void fill_foo(int* foo) { if (!foo) { // this is the NULL check printf("This is wrong\n"); return; } *foo = 23; } 

Bez sprawdzenia wartości NULL, przekazanie wskaźnika NULL do tej funkcji spowoduje segfault i nic nie możesz zrobić – system operacyjny po prostu zabije twój proces i może zrzuci rdzeń lub wyskoczy okno dialogowe raportu awarii. Z zerowym sprawdzeniem na miejscu, możesz przeprowadzić właściwą obsługę błędów i bezpiecznie naprawić – samodzielnie rozwiązać problem, przerwać bieżącą operację, napisać wpis w dzienniku, powiadomić użytkownika, cokolwiek jest właściwe.

Komentarze

  • @MrLister co masz na myśli, sprawdzanie wartości null nie ' nie działa w C ++? Musisz tylko zainicjować wskaźnik na wartość null, kiedy go deklarujesz.
  • Chodzi mi o to, że musisz pamiętać, aby ustawić wskaźnik na NULL lub wygrał ' t działa. A jeśli pamiętasz, innymi słowy, jeśli wiesz , że wskaźnik ma wartość NULL, wygrałeś ' i i tak nie musisz wywoływać funkcji fill_foo. fill_foo sprawdza, czy wskaźnik ma wartość, a nie czy wskaźnik ma prawidłową wartość. W C ++ nie ma gwarancji, że wskaźniki będą miały wartość NULL lub będą miały prawidłową wartość.
  • Funkcja assert () byłaby tutaj lepszym rozwiązaniem. ' nie ma sensu próbować ” być bezpiecznym „. Jeśli przekazano NULL, to ' jest ewidentnie błędne, więc dlaczego po prostu nie upaść jawnie, aby programista był w pełni świadomy? (A w produkcji nie ' nie ma to znaczenia, ponieważ ' ve udowodniono , że nikt nie zadzwoni do fill_foo () z wartością NULL, prawda? Naprawdę ' nie jest to trudne.)
  • Nie ' nie zapomnij aby wspomnieć, że jeszcze lepsza wersja tej funkcji powinna używać odwołań zamiast wskaźników, co sprawi, że sprawdzanie wartości NULL stanie się przestarzałe.
  • Nie o to chodzi w ręcznym zarządzaniu pamięcią, a zarządzany program też się rozpali lub zgłoś przynajmniej wyjątek, tak jak program natywny w większości języków), jeśli spróbujesz usunąć odwołanie o wartości null.

Odpowiedź

Pozostałe odpowiedzi w zasadzie pokrywały się z dokładnym pytaniem. Zostaje wykonane zerowe sprawdzenie, aby upewnić się, że otrzymany wskaźnik faktycznie wskazuje prawidłową instancję typu (obiekty, prymitywy itp.).

Zamierzam dodać tutaj własną radę, Unikaj sprawdzania wartości null. 🙂 Sprawdzanie wartości null (i inne formy programowania obronnego) zaśmieca kod i sprawia, że jest on bardziej podatny na błędy niż inne techniki obsługi błędów.

Moja ulubiona technika, jeśli chodzi o wskaźniki obiektów mają używać wzorca obiektu o wartości Null . Oznacza to zwrócenie (wskaźnik – lub nawet lepiej, odwołanie do) pustej tablicy lub listy zamiast wartości null, lub zwracanie pustego ciągu („”) zamiast null, lub nawet ciągu „0” (lub czegoś równoważnego „nic” w kontekście), jeśli oczekuje się, że zostanie on przeanalizowany na liczbę całkowitą.

Jako bonus, oto coś, czego mogłeś nie wiedzieć o wskaźniku zerowym, który został (po raz pierwszy formalnie) zaimplementowany przez CAR Hoare dla języka Algol W w 1965 roku.

Nazywam to moim miliardowym błędem. Był to wynalazek zerowej referencji w 1965 roku. W tamtym czasie projektowałem pierwszy kompleksowy system typów dla odwołań w obiekcie zorientowanym językiem (ALGOL W). Moim celem było zapewnienie, że każde użycie odniesień powinno być całkowicie bezpieczne, ze sprawdzaniem wykonywanym automatycznie przez kompilator. Ale nie mogłem oprzeć się pokusie wstawienia odniesienia zerowego, po prostu dlatego, że tak było łatwe do wdrożenia. Doprowadziło to do niezliczonych błędów, luk i awarii systemu, które prawdopodobnie spowodowały miliard dolarów bólu i szkód w ciągu ostatnich czterdziestu lat.

Komentarze

  • Obiekt zerowy jest nawet gorszy niż zwykłe posiadanie pustego wskaźnika. Jeśli algorytm X wymaga danych Y, których nie masz, to jest to błąd w twoim programie , który po prostu ukrywasz udając, że to robisz.
  • To zależy od kontekst i tak czy inaczej testowanie ” obecności danych ” przewyższa testowanie null w mojej książce. Z mojego doświadczenia wynika, że jeśli algorytm działa, powiedzmy, na liście, a lista jest pusta, to algorytm po prostu nie ma nic do zrobienia i dokonuje tego za pomocą standardowych instrukcji sterujących, takich jak for / foreach.
  • Jeśli algorytm nie ma nic wspólnego, dlaczego w ogóle go wywołujesz? Powodem, dla którego mógłbyś chcieć to nazwać w pierwszej kolejności, jest , ponieważ robi coś ważnego .
  • @DeadMG Ponieważ programy dotyczą wprowadzania danych, a w prawdziwym świecie, w przeciwieństwie do zadania domowe, dane wejściowe mogą być nieistotne (np. puste). Kod nadal jest wywoływany w obie strony. Masz dwie możliwości: albo sprawdzasz trafność (lub pustkę), albo projektujesz swoje algorytmy tak, aby dobrze się czytały i działały bez jawnego sprawdzania trafności za pomocą instrukcji warunkowych.
  • Przyszedłem tutaj, aby prawie ten sam komentarz, więc zamiast tego oddałem ci mój głos. Jednak chciałbym również dodać, że jest to reprezentatywne dla większego problemu obiektów zombie – za każdym razem, gdy masz obiekty z wieloetapową inicjalizacją (lub zniszczeniem), które nie są w pełni żywe, ale nie całkiem martwe. Gdy zobaczysz ” bezpieczny ” kod w językach bez deterministycznej finalizacji, który dodał sprawdzenia w każdej funkcji w celu sprawdzenia, czy obiekt został usunięty, to jest ten ogólny problem z wychowywaniem głowy '. Nigdy nie powinieneś, jeśli-null, powinieneś pracować ze stanami, które mają obiekty, których potrzebują przez całe życie.

Odpowiedz

Wartość pustego wskaźnika oznacza dobrze zdefiniowane „nigdzie”; jest to nieprawidłowa wartość wskaźnika, która gwarantuje nierówne porównanie z jakąkolwiek inną wartością wskaźnika. Próba wyłuskiwania wskaźnika zerowego skutkuje niezdefiniowanym zachowaniem i zwykle prowadzi do błędu w czasie wykonywania, więc przed przystąpieniem do wyłuskiwania należy się upewnić, że wskaźnik nie ma wartości NULL. Szereg funkcji bibliotecznych C i C ++ zwróci wskaźnik zerowy, aby wskazać stan błędu. Na przykład funkcja biblioteki malloc zwróci wartość pustego wskaźnika, jeśli nie może przydzielić żądanej liczby bajtów, a próba uzyskania dostępu do pamięci za pomocą tego wskaźnika (zwykle) prowadzi do błędu czasu wykonania:

int *p = malloc(sizeof *p * N); p[0] = ...; // this will (usually) blow up if malloc returned NULL 

Dlatego musimy się upewnić, że wywołanie malloc powiodło się, sprawdzając wartość p przeciwko NULL:

int *p = malloc(sizeof *p * N); if (p != NULL) // or just if (p) p[0] = ...; 

Teraz poczekaj chwilę na skarpetki, to da nieco wyboista.

Istnieje pusty wskaźnik wartość i pusty wskaźnik stała , a te dwa elementy niekoniecznie są takie same. Wskaźnik null wartość jest dowolną wartością, której podstawowa architektura używa do reprezentowania „nigdzie”. Ta wartość może być 0x00000000, 0xFFFFFFFF, 0xDEADBEEF lub czymś zupełnie innym. Nie zakładaj, że wskaźnik zerowy wartość zawsze wynosi 0.

Wskaźnik zerowy stała , OTOH, jest zawsze wyrażeniem całkowitym o wartości 0. Jeśli chodzi o twój kod źródłowy , 0 (lub dowolne wyrażenie całkowite, którego wynikiem jest 0) reprezentuje wskaźnik pusty. Zarówno C, jak i C ++ definiują makro NULL jako stałą pustego wskaźnika. Kiedy twój kod jest kompilowany, pusty wskaźnik stała zostanie zastąpiony odpowiednią wartością wskaźnika pustego w wygenerowanym kodzie maszynowym.

Pamiętaj też, że NULL jest tylko jedną z wielu możliwych nieprawidłowych wartości wskaźnika; jeśli zadeklarujesz zmienną automatycznego wskaźnika bez jawnej jej inicjalizacji, na przykład

int *p; 

wartość początkowo przechowywana w zmiennej jest nieokreślona , i może nie odpowiadać poprawnemu lub dostępnemu adresowi pamięci. Niestety, nie ma (przenośnego) sposobu, aby stwierdzić, czy wartość wskaźnika inna niż NULL jest poprawna, czy nie, przed próbą jej użycia. Więc jeśli masz do czynienia ze wskaźnikami, zwykle dobrym pomysłem jest jawne zainicjowanie ich NULL, gdy je zadeklarujesz, i ustawić je na NULL, gdy nie wskazują aktywnie na nic.

Zauważ, że jest to większy problem w C niż C ++; idiomatyczny C ++ nie powinien zbyt często używać wskaźników.

Odpowiedź

Jest kilka metod, wszystkie zasadniczo robią to samo rzecz.

 int *foo = NULL; //sometimes set to 0x00 or 0 or 0L instead of NULL 

sprawdzenie null (sprawdź, czy wskaźnik jest pusty), wersja A

 if( foo == NULL) 

sprawdzanie zerowe, wersja B

 if( !foo ) //since NULL is defined as 0, !foo will return a value from a null pointer 

sprawdzanie zerowe, wersja C

 if( foo == 0 ) 

Spośród tych trzech, wolę użyć pierwszego sprawdzenia, ponieważ wyraźnie mówi przyszłym programistom, czego próbujesz sprawdzić ORAZ jasno pokazuje, że spodziewałeś się, że foo będzie wskaźnikiem.

Odpowiedź

Nie. Jedynym powodem używania wskaźnika w C ++ jest to, że jawnie chcesz, aby wskaźniki były puste; w przeciwnym razie możesz wziąć odniesienie, które jest zarówno semantycznie łatwiejsze w użyciu, jak i gwarantuje wartość różną od null.

Komentarze

  • @James: ' new ' w trybie jądra?
  • @James: implementacja C ++, która reprezentuje możliwości, które znaczna większość C ++ koderów lubi. Obejmuje to wszystkie funkcje języka C ++ 03 (z wyjątkiem export) i wszystkie funkcje biblioteki C ++ 03 i TR1 i niezła część C ++ 11.
  • Nie chciałbym, żeby ludzie ' nie mówili, że ” referencje gwarantują wartość różną od zera. ” Nie ' t. Generowanie odniesienia zerowego jest równie łatwe jak wskaźnik zerowy, a one propagują się w ten sam sposób.
  • @Stargazer: Pytanie jest w 100% zbędne, gdy używasz narzędzi tak, jak projektanci języka i dobrze praktyka sugeruje, że powinieneś.
  • @DeadMG, to nie ' nie ma znaczenia, czy jest zbędne. nie ' nie odpowiedziałeś na pytanie . I ' powiem to jeszcze raz: -1.

Odpowiedź

Jeśli nie sprawdzisz wartości NULL, szczególnie jeśli jest to wskaźnik do struktury, być może napotkałeś lukę w zabezpieczeniach – wyłuskiwanie wskaźnika NULL. Dereferencja wskaźnika NULL może prowadzić do innych poważnych luk w zabezpieczeniach, takich jak przepełnienie bufora, sytuacja wyścigu … która może pozwolić atakującemu przejąć kontrolę nad twoim komputerem.

Wielu dostawców oprogramowania, takich jak Microsoft, Oracle, Adobe, Apple … wypuszcza poprawkę oprogramowania, aby naprawić te luki w zabezpieczeniach. Myślę, że powinieneś sprawdź wartość NULL każdego wskaźnika 🙂

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *