Co to znaczy „zerować” w C lub C ++?

21

Uczę się języka C ++ i trudno mi zrozumieć zero. W szczególności w samouczkach, które przeczytałem, wspomniano o „zerowym sprawdzaniu”, ale nie jestem pewien, co to oznacza ani dlaczego jest konieczne.

  • Czym dokładnie jest zero?
  • Co to znaczy „sprawdzić, czy nie ma nic”?
  • Czy zawsze muszę sprawdzać, czy nie ma wartości null?

Wszelkie przykłady kodu będą mile widziane.

kdi
źródło
Kiedy: programmers.stackexchange.com/questions/186036/...
Ciro Santilli 新疆 改造 中心 法轮功 六四 事件
Radziłbym zdobyć kilka lepszych samouczków, jeśli wszystkie, które czytasz, mówią o zerowych czekach, nigdy ich nie wyjaśniając i nie podając przykładowego kodu ...
underscore_d

Odpowiedzi:

26

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

Częstym rozwiązaniem, które wychwytuje wiele błędów, jest ustawianie wszystkich wskaźników, które nie wskazują na nic jako NULL(lub, w poprawnym C ++ 0), i sprawdzanie tego 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 można je wskazać, kiedy je deklarujesz) i ustawianie ich na NULL, gdy ty deletelub free()oni (chyba, że ​​natychmiast przestaną być objęte zakresem). Przykład (w C, ale także 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 kontroli zerowej przekazanie wskaźnika NULL do tej funkcji spowoduje awarię segregacji i nic nie możesz zrobić - system operacyjny po prostu zabije Twój proces i być może zrzut pamięci lub wyświetli okno dialogowe raportu o awarii. Po wprowadzeniu kontroli zerowej możesz prawidłowo obsługiwać błędy i odzyskać wdzięczność - samodzielnie rozwiąż problem, przerwij bieżącą operację, napisz wpis w dzienniku, powiadom użytkownika o wszystkim, co jest odpowiednie.

tdammers
źródło
3
@MrLister, co masz na myśli, sprawdzanie wartości NULL nie działa w C ++? Musisz tylko zainicjować wskaźnik na wartość zerową, kiedy go deklarujesz.
TZHX
1
Chodzi mi o to, że musisz pamiętać o ustawieniu wskaźnika na NULL, inaczej nie zadziała. A jeśli pamiętasz, innymi słowy, jeśli wiesz, że wskaźnik ma wartość NULL, i tak nie będziesz musiał 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 mają wartość NULL lub mają prawidłową wartość.
Pan Lister,
4
Lepszym rozwiązaniem byłoby tutaj assert (). Nie ma sensu próbować „być bezpiecznym”. Jeśli podano NULL, jest to oczywiście błąd, więc dlaczego po prostu nie upaść jawnie, aby programista był w pełni świadomy? (A w produkcji to nie ma znaczenia, ponieważ udowodniłeś, że nikt nie wywoła fill_foo () z NULL, prawda? Naprawdę, to nie jest takie trudne.)
Ambroz Bizjak
7
Nie zapomnij wspomnieć, że nawet lepsza wersja tej funkcji powinna używać referencji zamiast wskaźników, dzięki czemu sprawdzanie NULL staje się nieaktualne.
Doc Brown
4
Nie o to chodzi w ręcznym zarządzaniu pamięcią, a program zarządzany również wysadzi w powietrze (lub podniesie wyjątek, tak jak program macierzysty w większości języków), jeśli spróbujesz wyrejestrować odwołanie zerowe.
Mason Wheeler,
7

Pozostałe odpowiedzi w zasadzie dotyczyły twojego dokładnego pytania. Kontrola zerowa ma na celu upewnienie się, że otrzymany wskaźnik wskazuje prawidłową instancję typu (obiekty, operacje podstawowe itp.).

Jednak dodam tutaj swoją własną radę. Unikaj kontroli zerowej. :) Kontrola zerowa (i inne formy programowania defensywnego) zaśmiecają kod i sprawiają, że jest on bardziej podatny na błędy niż inne techniki obsługi błędów.

Moją ulubioną techniką, jeśli chodzi o wskaźniki obiektów, jest użycie wzorca Null Object . Oznacza to, że zwracamy (wskaźnik - lub nawet lepiej, odwołanie do) pustą tablicę lub listę zamiast null, lub zwracamy pusty ciąg („”) zamiast null, a nawet ciąg „0” (lub coś równoważnego „nic” „w kontekście), gdzie oczekuje się, że zostanie on sparsowany do liczby całkowitej.

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 błędem za miliard dolarów. Był to wynalazek referencji zerowej w 1965 roku. W tym czasie projektowałem pierwszy kompleksowy system typów dla referencji w języku obiektowym (ALGOL W). Moim celem było upewnienie się, że każde użycie referencji powinno być całkowicie bezpieczne, a sprawdzanie wykonywane automatycznie przez kompilator. Ale nie mogłem oprzeć się pokusie wprowadzenia zerowego odniesienia, po prostu dlatego, że było to tak łatwe do wdrożenia. Doprowadziło to do niezliczonych błędów, podatności i awarii systemu, które prawdopodobnie spowodowały miliard dolarów bólu i szkód w ciągu ostatnich czterdziestu lat.

Yam Marcovic
źródło
6
Obiekt zerowy jest nawet gorszy niż zwykły wskaźnik zerowy. 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.
DeadMG,
Zależy to od kontekstu, a testowanie „obecności danych” w obu przypadkach w mojej książce przebija testowanie na wartość null. Z mojego doświadczenia wynika, że ​​jeśli algorytm działa na, powiedzmy, liście, a lista jest pusta, to algorytm po prostu nie ma nic do roboty i osiąga to dzięki zwykłym instrukcjom sterującym, takim jak for / foreach.
Yam Marcovic
Jeśli algorytm nie ma nic wspólnego, to dlaczego go w ogóle wywołujesz? A powodem, dla którego mógłbyś chcieć to nazwać, jest to, że robi coś ważnego .
DeadMG,
@DeadMG Ponieważ w programach chodzi o dane wejściowe, w rzeczywistości, w przeciwieństwie do zadań domowych, dane wejściowe mogą być nieistotne (np. Puste). Kod nadal jest wywoływany w obu kierunkach. Masz dwie opcje: albo sprawdzasz trafność (lub pustkę), albo projektujesz swoje algorytmy tak, aby czytały i działały dobrze bez wyraźnego sprawdzania trafności za pomocą instrukcji warunkowych.
Yam Marcovic
Przybyłem tutaj, aby zrobić prawie ten sam komentarz, więc oddałem ci mój głos. Dodam jednak, ż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. Kiedy widzisz „bezpieczny” kod w językach bez deterministycznej finalizacji, która dodała kontrole w każdej funkcji, aby sprawdzić, czy obiekt został usunięty, jest to ogólny problem z podniesieniem głowy. Nigdy nie powinieneś, jeśli-zero, powinieneś pracować ze stanami, które mają obiekty, których potrzebują na całe życie.
ex0du5
4

Wartość wskaźnika zerowego reprezentuje dobrze zdefiniowane „nigdzie”; jest to niepoprawna wartość wskaźnika, która gwarantuje porównanie nierówne z dowolną inną wartością wskaźnika. Próba wyrejestrowania wskaźnika zerowego powoduje niezdefiniowane zachowanie i zwykle prowadzi do błędu środowiska wykonawczego, dlatego przed upewnieniem się, że wskaźnik nie ma wartości NULL, należy upewnić się, że wskaźnik nie ma wartości NULL. Szereg funkcji bibliotecznych C i C ++ zwróci wskaźnik zerowy wskazujący warunek błędu. Na przykład funkcja biblioteczna malloczwróci wartość wskaźnika zerowego, jeśli nie będzie mogła przydzielić żądanej liczby bajtów, a próba uzyskania dostępu do pamięci przez ten wskaźnik (zwykle) doprowadzi do błędu w czasie wykonywania:

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

Musimy więc upewnić się, że mallocwywołanie zakończyło się powodzeniem, sprawdzając wartość pprzeciw NULL:

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

A teraz poczekaj chwilę na skarpetkach, to będzie trochę wyboiste.

Istnieje wskaźnik NULL wartość i null pointer stałe , a dwa nie zawsze są takie same. Null pointer wartość jest niezależnie od wartości leżących u podstaw architektury zastosowania do reprezentowania „nigdzie”. Ta wartość może wynosić 0x00000000 lub 0xFFFFFFFF, 0xDEADBEEF lub coś zupełnie innego. Nie należy zakładać, że wskaźnik zerowa wartość jest zawsze 0.

Stała wskaźnika zerowego , OTOH, jest zawsze wyrażeniem całkowitym o wartości 0. Jeśli chodzi o kod źródłowy , 0 (lub dowolne wyrażenie całkowite, którego wynikiem jest 0), oznacza wskaźnik zerowy. Zarówno C, jak i C ++ definiują makro NULL jako stałą wskaźnika zerowego. Gdy kod zostanie skompilowany, null pointer stałe zostaną zastąpione odpowiednim zerowy wskaźnik wartości w wygenerowanego kodu maszynowego.

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

int *p;

wartość początkowo przechowywana w zmiennej jest nieokreślona i może nie odpowiadać prawidłowemu 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 na NULL, kiedy je deklarujesz, i ustawienie ich na NULL, gdy nie wskazują na nic aktywnie.

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

John Bode
źródło
3

Istnieje kilka metod, wszystkie w zasadzie robią to samo.

int * foo = NULL; // czasami ustawione na 0x00 lub 0 lub 0L zamiast NULL

kontrola zerowa (sprawdź, czy wskaźnik jest pusty), wersja A

if (foo == NULL)

kontrola zerowa, wersja B

if (! foo) // ponieważ NULL jest zdefiniowane jako 0,! foo zwróci wartość ze wskaźnika zerowego

kontrola zerowa, wersja C

if (foo == 0)

Z tych trzech wolę użyć pierwszego czeku, ponieważ wyraźnie mówi przyszłym programistom, co próbujesz sprawdzić ORAZ wyjaśnia, że ​​spodziewałeś się, że foo będzie wskaźnikiem.


źródło
2

Ty nie. Jedynym powodem użycia wskaźnika w C ++ jest to, że jawnie chcesz obecność wskaźników zerowych; w przeciwnym razie możesz wziąć odwołanie, które jest semantycznie łatwiejsze w użyciu i gwarantuje wartość inną niż null.

DeadMG
źródło
1
@James: „nowy” w trybie jądra?
Nemanja Trifunovic
1
@James: Implementacja C ++, która reprezentuje możliwości, z których korzysta znaczna większość programistów C ++. Obejmuje to wszystkie funkcje języka C ++ 03 (oprócz export) oraz wszystkie funkcje biblioteki C ++ 03 oraz TR1 i sporą część C ++ 11.
DeadMG
5
Chciałbym , żeby ludzie nie mówili, że „referencje gwarantują wartość inną niż zero”. Oni nie. Wygenerowanie odwołania zerowego jest równie łatwe jak wskaźnika zerowego i propagują się one w ten sam sposób.
mjfgates,
2
@Stargazer: Pytanie jest w 100% zbędne, gdy używasz narzędzi w sposób, w jaki sugerują to projektanci języków i dobre praktyki.
DeadMG,
2
@DeadMG, nie ma znaczenia, czy jest zbędne. Nie odpowiedziałeś na pytanie . Powiem to jeszcze raz: -1.
riwalk
-1

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

Wielu dostawców oprogramowania, takich jak Microsoft, Oracle, Adobe, Apple ... wydaje poprawki oprogramowania, aby naprawić te luki w zabezpieczeniach. Myślę, że powinieneś sprawdzić NULL wartość każdego wskaźnika :)

oDisPo
źródło