Ustawienie zmiennej na NULL po zwolnieniu

156

W mojej firmie obowiązuje zasada kodowania, która mówi, że po zwolnieniu pamięci należy zresetować zmienną do NULL. Na przykład ...

void some_func () 
{
    int *nPtr;

    nPtr = malloc (100);

    free (nPtr);
    nPtr = NULL;

    return;
}

Czuję, że w przypadkach takich jak w kodzie pokazanym powyżej, ustawienie na NULLnie ma żadnego znaczenia. A może coś mi brakuje?

Jeśli nie ma to sensu w takich przypadkach, zamierzam porozmawiać z „zespołem ds. Jakości” o usunięcie tej reguły kodowania. Proszę o poradę.

Alphaneo
źródło
2
zawsze warto mieć możliwość sprawdzenia, czy ptr == NULLprzed zrobieniem cokolwiek z tym. Jeśli nie anulujesz swoich bezpłatnych wskaźników, otrzymasz, ptr != NULLale nadal nieużywalny wskaźnik.
Ki Jéy,
Wiszące wskaźniki mogą prowadzić do możliwych do wykorzystania luk w zabezpieczeniach, takich jak Use-After-Free .
Константин Ван

Odpowiedzi:

285

Ustawienie nieużywanych wskaźników na NULL jest stylem obronnym, chroniącym przed wiszącymi błędami wskaźnika. W przypadku uzyskania dostępu do wiszącego wskaźnika po jego zwolnieniu można odczytać lub nadpisać pamięć losową. W przypadku uzyskania dostępu do pustego wskaźnika w większości systemów występuje natychmiastowa awaria, informując od razu o błędzie.

W przypadku zmiennych lokalnych może to być trochę bezcelowe, jeśli jest „oczywiste”, że wskaźnik nie jest już używany po zwolnieniu, więc ten styl jest bardziej odpowiedni dla danych składowych i zmiennych globalnych. Nawet w przypadku zmiennych lokalnych dobrym podejściem może być kontynuacja funkcji po zwolnieniu pamięci.

Aby ukończyć styl, powinieneś również zainicjować wskaźniki na NULL, zanim zostaną przypisane im prawdziwa wartość wskaźnika.

Martin przeciwko Löwis
źródło
3
Nie rozumiem, dlaczego miałbyś „inicjować wskaźniki na NULL, zanim zostaną przypisane im prawdziwa wartość wskaźnika”?
Paul Biggar,
26
@Paul: W tym konkretnym przypadku deklaracja mogła zostać odczytana int *nPtr=NULL;. Teraz zgodziłbym się, że byłoby to zbędne, z malloc po prawej stronie w następnej linii. Jeśli jednak między deklaracją a pierwszą inicjalizacją znajduje się kod, ktoś może zacząć używać zmiennej, mimo że nie ma ona jeszcze wartości. Jeśli zainicjujesz wartość null, otrzymasz segfault; bez, możesz ponownie czytać lub zapisywać pamięć losową. Podobnie, jeśli zmienna zostanie później zainicjalizowana tylko warunkowo, późniejsze błędne dostępy powinny spowodować natychmiastowe awarie, jeśli pamiętasz o inicjalizacji zerowej.
Martin przeciwko Löwis
1
Osobiście uważam, że w jakiejkolwiek niebanalnej bazie kodów otrzymanie błędu przy dereferencji null jest tak niejasne, jak uzyskanie błędu przy wyłuskiwaniu adresu, którego nie jesteś właścicielem. Osobiście nigdy się nie przejmuję.
wilhelmtell
9
Wilhelm, chodzi o to, że przy dereferencji wskaźnika zerowego otrzymujesz określoną awarię i rzeczywistą lokalizację problemu. Zły dostęp może, ale nie musi, ulec awarii oraz uszkodzić dane lub zachowanie w nieoczekiwany sposób w nieoczekiwanych miejscach.
Amit Naidu
4
Właściwie inicjalizacja wskaźnika do NULL ma co najmniej jedną istotną wadę: może uniemożliwić kompilatorowi ostrzeżenie o niezainicjowanych zmiennych. O ile logika twojego kodu faktycznie nie obsługuje tej wartości wskaźnika (tj. Jeśli (nPtr == NULL) coś ...), lepiej zostawić to tak, jak jest.
Eric
37

Ustawienie wskaźnika na NULLafter freejest wątpliwą praktyką, która jest często spopularyzowana jako zasada „dobrego programowania” oparta na ewidentnie fałszywej przesłance. To jedna z tych fałszywych prawd, które należą do kategorii „brzmi dobrze”, ale w rzeczywistości nie przynoszą absolutnie nic pożytecznego (i czasami prowadzą do negatywnych konsekwencji).

Podobno ustawienie wskaźnika na NULLafter freema zapobiec przerażającemu problemowi „podwójnego zwolnienia”, gdy ta sama wartość wskaźnika jest przekazywana freewięcej niż raz. W rzeczywistości jednak w 9 przypadkach na 10 rzeczywisty problem „podwójnego zwolnienia” występuje, gdy różne obiekty wskaźnika posiadające tę samą wartość wskaźnika są używane jako argumenty for free. Nie trzeba dodawać, że ustawienie wskaźnika na NULLafter nie freedaje absolutnie nic, co mogłoby zapobiec problemowi w takich przypadkach.

Oczywiście można napotkać problem „podwójnego zwolnienia”, używając tego samego obiektu wskaźnika jako argumentu funkcji free. Jednak w rzeczywistości sytuacje takie jak ta zwykle wskazują na problem z ogólną logiczną strukturą kodu, a nie na zwykłe przypadkowe „podwójne wolne”. W takich przypadkach właściwym sposobem rozwiązania problemu jest przejrzenie i przemyślenie struktury kodu w celu uniknięcia sytuacji, w której ten sam wskaźnik jest przekazywany freewięcej niż raz. W takich przypadkach ustawienie wskaźnika NULLi uznanie problemu za „naprawiony” to nic innego jak próba zamiatania problemu pod dywan. Po prostu nie zadziała w ogólnym przypadku, ponieważ problem ze strukturą kodu zawsze znajdzie inny sposób, aby się ujawnić.

Wreszcie, jeśli Twój kod jest specjalnie zaprojektowany, aby polegać na wartości wskaźnika będącej NULLlub nie NULL, ustawienie wartości wskaźnika na NULLafter jest całkowicie w porządku free. Ale zgodnie z ogólną zasadą „dobrej praktyki” (jak w przypadku „zawsze ustawiaj wskaźnik na NULLpo free”) jest to znowu dobrze znana i całkiem bezużyteczna podróbka, często stosowana z czysto religijnych powodów podobnych do voodoo.

Mrówka
źródło
1
Zdecydowanie. Nie pamiętam, żebym kiedykolwiek spowodował podwójne zwolnienie, które zostałoby naprawione przez ustawienie wskaźnika na NULL po zwolnieniu, ale spowodowałem wiele, że tak by nie było.
LnxPrgr3
4
@AnT "wątpliwe" to trochę za dużo. Wszystko zależy od przypadku użycia. Jeśli wartość wskaźnika jest kiedykolwiek używana w sensie prawda / fałsz, jest to nie tylko prawidłowa praktyka, ale także najlepsza praktyka.
Coder,
1
@Coder Całkowicie źle. Jeśli wartość wskaźnika jest używana w prawdziwym fałszywym sensie, aby wiedzieć, czy wskazywał obiekt przed wywołaniem free, nie tylko nie jest to najlepsza praktyka, ale jest błędna . Na przykład: foo* bar=getFoo(); /*more_code*/ free(bar); /*more_code*/ return bar != NULL;. Tutaj ustawienie baraby NULLpo wywołaniu freespowoduje funkcję myśleć, że nigdy nie miał bar i zwróci błędną wartość!
David Schwartz,
Nie sądzę, aby podstawową korzyścią była ochrona przed podwójnym wolnym, a raczej szybsze i bardziej niezawodne łapanie wiszących wskazówek. Na przykład, kiedy zwalniam strukturę, która przechowuje zasoby, wskaźniki do przydzielonej pamięci, uchwyty plików itp., Gdy zwalniam wskaźniki pamięci zawarte i zamykam zawarte w nich pliki, NULLUJĘ odpowiednie elementy. Następnie, jeśli przez pomyłkę uzyskano dostęp do jednego z zasobów za pośrednictwem wiszącego wskaźnika, program za każdym razem ma tendencję do błędu. W przeciwnym razie, bez NULLing, uwolnione dane mogą nie zostać jeszcze nadpisane, a błąd może nie być łatwo odtworzony.
jimhark,
1
Zgadzam się, że dobrze skonstruowany kod nie powinien zezwalać na przypadek, w którym wskaźnik jest dostępny po zwolnieniu lub w przypadku, gdy jest on uwalniany dwa razy. Ale w prawdziwym świecie mój kod będzie modyfikowany i / lub utrzymywany przez kogoś, kto prawdopodobnie mnie nie zna i nie ma czasu i / lub umiejętności, aby zrobić wszystko poprawnie (ponieważ termin jest zawsze wczoraj). Dlatego mam tendencję do pisania kuloodpornych funkcji, które nie powodują awarii systemu, nawet jeśli są niewłaściwie używane.
mfloris
34

Większość odpowiedzi skupiała się na zapobieganiu podwójnemu zwolnieniu, ale ustawienie wskaźnika na NULL ma jeszcze jedną korzyść. Po zwolnieniu wskaźnika pamięć ta jest dostępna do ponownego przydzielenia przez kolejne wywołanie malloc. Jeśli nadal masz oryginalny wskaźnik dookoła, możesz skończyć z błędem, w którym będziesz próbował użyć wskaźnika po zwolnieniu i uszkodzeniu innej zmiennej, a wtedy twój program wejdzie w nieznany stan i mogą się zdarzyć różne rodzaje złych rzeczy (awaria, jeśli masz szczęście, uszkodzenie danych, jeśli masz pecha). Gdybyś ustawił wskaźnik na NULL po zwolnieniu, każda próba odczytu / zapisu przez ten wskaźnik później spowodowałaby błąd segfault, który jest generalnie lepszy niż przypadkowe uszkodzenie pamięci.

Z obu powodów dobrym pomysłem może być ustawienie wskaźnika na NULL po funkcji free (). Jednak nie zawsze jest to konieczne. Na przykład, jeśli zmienna wskaźnikowa wychodzi poza zakres natychmiast po funkcji free (), nie ma powodu, aby ustawiać ją na NULL.

Mike McNertney
źródło
1
+1 To jest właściwie bardzo dobra uwaga. Nie rozumowanie o „podwójnie darmowym” (co jest całkowicie fałszywe), ale to . Nie jestem fanem mechanicznego zerowania wskaźników później free, ale to ma sens.
AnT
Gdybyś mógł uzyskać dostęp do wskaźnika po zwolnieniu go przez ten sam wskaźnik, jest jeszcze bardziej prawdopodobne, że uzyskasz dostęp do wskaźnika po zwolnieniu obiektu, na który wskazuje, przez inny wskaźnik. Więc to wcale ci nie pomaga - nadal musisz użyć innego mechanizmu, aby upewnić się, że nie uzyskasz dostępu do obiektu przez jeden wskaźnik po zwolnieniu go przez inny. Równie dobrze możesz użyć tej metody do ochrony w tym samym przypadku wskaźnika.
David Schwartz
1
@DavidSchwartz: Nie zgadzam się z twoim komentarzem. Kiedy kilka tygodni temu musiałem napisać stos do ćwiczeń uniwersyteckich, miałem problem, zbadałem kilka godzin. W pewnym momencie uzyskałem dostęp do już zwolnionej pamięci (wolna była o kilka linii za wcześnie). Czasami prowadzi to do bardzo dziwnych zachowań. Gdybym po zwolnieniu go ustawił wskaźnik na NULL, wystąpiłby „prosty” segfault i zaoszczędziłbym kilka godzin pracy. Więc +1 za tę odpowiedź!
mozzbozz
2
@katze_sonne Nawet zatrzymany zegar dwa razy dziennie wskazuje dokładność. O wiele bardziej prawdopodobne jest, że ustawienie wskaźników na NULL ukryje błędy, zapobiegając błędnym dostępom do już zwolnionych obiektów przed segfaultowaniem w kodzie, który sprawdza wartość NULL, a następnie po cichu nie sprawdza obiektu, który powinien był sprawdzić. (Być może ustawienie wskaźników na NULL po zwolnieniu w określonych kompilacjach debugowania może być pomocne lub ustawienie ich na wartość inną niż NULL, która gwarantuje segfault, może mieć sens. Ale to, że ta głupota pomogła ci raz, nie jest argumentem na jego korzyść .)
David Schwartz
@DavidSchwartz Cóż, to brzmi rozsądnie ... Dzięki za komentarz, rozważę to w przyszłości! :) +1
mozzbozz
20

Jest to uważane za dobrą praktykę, aby uniknąć nadpisywania pamięci. W powyższej funkcji jest to niepotrzebne, ale często kiedy jest to zrobione, może znaleźć błędy aplikacji.

Zamiast tego spróbuj czegoś takiego:

#if DEBUG_VERSION
void myfree(void **ptr)
{
    free(*ptr);
    *ptr = NULL;
}
#else
#define myfree(p) do { void ** __p = (p); free(*(__p)); *(__p) = NULL; } while (0)
#endif

DEBUG_VERSION umożliwia zwolnienie profilu w kodzie debugowania, ale oba są funkcjonalnie takie same.

Edycja : Dodano do ... podczas gdy zgodnie z sugestią poniżej, dzięki.

zrównany
źródło
3
Wersja makra ma subtelny błąd, jeśli użyjesz go po instrukcji if bez nawiasów.
Mark Ransom
O co chodzi z (pustką) 0? Ten kod robi: if (x) myfree (& x); else do_foo (); staje się if (x) {free (* (& x)); * (& x) = null; } void 0; else do_foo (); Innym jest błąd.
jmucchiello
To makro jest idealnym miejscem dla operatora przecinka: free ( (p)), * (p) = null. Oczywiście następny problem polega na tym, że dwukrotnie oblicza * (p). Powinien być {void * _pp = (p); wolny (* _ pp); * _pp = null; } Czy preprocesor nie jest zabawny?
jmucchiello
5
Makro nie powinno być w pustych nawiasach, powinno znajdować się w do { } while(0)bloku, aby if(x) myfree(x); else dostuff();się nie zepsuło.
Chris Lutz,
3
Jak powiedział Lutz, ciało makra do {X} while (0)to IMO najlepszy sposób na stworzenie ciała makra, które „czuje się i działa jak” funkcja. Większość kompilatorów i tak optymalizuje pętlę.
Mike Clark
7

Jeśli dojdziesz do wskaźnika, który był wolny () d, może się zepsuć lub nie. Ta pamięć może zostać ponownie przydzielona do innej części programu, a następnie wystąpi uszkodzenie pamięci,

Jeśli ustawisz wskaźnik na NULL, a następnie uzyskasz do niego dostęp, program zawsze zawiesza się z błędem segfault. Koniec z `` czasami działa '', koniec z `` awariami w nieprzewidywalny sposób ''. O wiele łatwiej jest debugować.

Tadeusz A. Kadłubowski
źródło
5
Program nie zawsze kończy się awarią. Jeśli sposób uzyskiwania dostępu do wskaźnika oznacza, że ​​przed dereferencją zastosowano do niego wystarczająco duże przesunięcie, może on dotrzeć do pamięci adresowalnej: ((MyHugeStruct *) 0) -> fieldNearTheEnd. I to jeszcze zanim zajmiesz się sprzętem, który w ogóle nie narusza dostępu 0. Jednak istnieje większe prawdopodobieństwo, że program ulegnie awarii z powodu segfault.
Steve Jessop
7

Ustawienie wskaźnika na freepamięć 'd oznacza, że ​​każda próba uzyskania dostępu do tej pamięci za pośrednictwem wskaźnika spowoduje natychmiastową awarię, zamiast powodować niezdefiniowane zachowanie. Znacznie ułatwia określenie, gdzie coś poszło nie tak.

Widzę twój argument: ponieważ nPtrzaraz potem wychodzi poza zakres nPtr = NULL, nie ma powodu, aby go ustawić NULL. Jednak w przypadku elementu structczłonkowskiego lub w innym miejscu, w którym wskaźnik nie wychodzi natychmiast poza zakres, ma to większy sens. Nie jest od razu jasne, czy ten wskaźnik zostanie ponownie użyty przez kod, który nie powinien go używać.

Prawdopodobnie reguła została podana bez rozróżnienia między tymi dwoma przypadkami, ponieważ znacznie trudniej jest automatycznie egzekwować regułę, nie mówiąc już o przestrzeganiu jej przez programistów. Nie zaszkodzi ustawiać wskaźniki NULLpo każdym wolnym, ale może wskazywać na duże problemy.

Jared Oberhaus
źródło
7

najczęstszym błędem w c jest podwójne wolne. Zasadniczo robisz coś takiego

free(foobar);
/* lot of code */
free(foobar);

i kończy się dość źle, system operacyjny próbuje uwolnić część już zwolnionej pamięci i generalnie powoduje awarię. Więc dobrą praktyką jest ustawienie na NULL, abyś mógł wykonać test i sprawdzić, czy naprawdę potrzebujesz zwolnić tę pamięć

if(foobar != null){
  free(foobar);
}

należy również zauważyć, że free(NULL)nic nie da, więc nie musisz pisać instrukcji if. Nie jestem guru systemów operacyjnych, ale nawet teraz jestem ładna większość systemów operacyjnych na podwójnej wolności.

Jest to również główny powód, dla którego wszystkie języki z wyrzucaniem elementów bezużytecznych (Java, dotnet) były tak dumne, że nie mają tego problemu, a także nie muszą pozostawiać programistom zarządzania pamięcią jako całości.

RageZ
źródło
11
W rzeczywistości możesz po prostu wywołać free () bez sprawdzania - free (NULL) jest zdefiniowane jako nic nie robienie.
Bursztynowy
5
Czy to nie ukrywa błędów? (Jak uwolnienie za dużo.)
Georg Schölly
1
dzięki, mam to. Próbowałem:p = (char *)malloc(.....); free(p); if(p!=null) //p!=null is true, p is not null although freed { free(p); //Note: checking doesnot prevent error here }
Shaobo Wang
5
Jak powiedziałem, free(void *ptr) nie można zmienić wartości przekazywanego wskaźnika. Może zmienić zawartość wskaźnika, dane przechowywane pod tym adresem , ale nie sam adres ani wartość wskaźnika . Wymagałoby to free(void **ptr)(co najwyraźniej nie jest dozwolone przez standard) lub makra (które jest dozwolone i doskonale przenośne, ale ludzie nie lubią makr). Ponadto w C nie chodzi o wygodę, ale o zapewnienie programistom takiej kontroli, jaką chcą. Jeśli nie chcą dodatkowego narzutu ustawiania wskaźników NULL, nie należy ich narzucać.
Chris Lutz,
2
Niewiele jest rzeczy na świecie, które zdradzają brak profesjonalizmu ze strony autora kodu C. Ale obejmują one „sprawdzanie wskaźnika dla NULL przed wywołaniem free” (razem z takimi rzeczami jak „rzutowanie wyniku funkcji alokacji pamięci” lub „bezmyślne używanie nazw typów z sizeof”).
AnT
6

Ideą tego jest powstrzymanie przypadkowego ponownego użycia uwolnionego wskaźnika.

Mitch Wheat
źródło
4

To (może) faktycznie być ważne. Chociaż zwalniasz pamięć, późniejsza część programu może przydzielić coś nowego, co zdarzy się wylądować w przestrzeni. Twój stary wskaźnik wskazywałby teraz prawidłowy fragment pamięci. Jest wtedy możliwe, że ktoś użyje wskaźnika, co spowoduje nieprawidłowy stan programu.

Jeśli wyzerujesz wskaźnik, każda próba jego użycia spowoduje wyłuskiwanie 0x0 i zawieszenie się w tym miejscu, co jest łatwe do debugowania. Przypadkowe wskaźniki wskazujące na pamięć losową są trudne do debugowania. Oczywiście nie jest to konieczne, ale dlatego jest to dokument zawierający najlepsze praktyki.

Steven Canfield
źródło
Przynajmniej w systemie Windows kompilacje debugowania ustawią pamięć na 0xdddddddd, więc kiedy używasz wskaźnika do usuniętej pamięci, wiesz od razu. Na wszystkich platformach powinny istnieć podobne mechanizmy.
i_am_jorf
2
jeffamaphone, usunięty blok pamięci mógł zostać ponownie przydzielony i przypisany do innego obiektu do czasu ponownego użycia wskaźnika.
Constantin,
4

Ze standardu ANSI C:

void free(void *ptr);

Funkcja free powoduje zwolnienie miejsca wskazywanego przez ptr, czyli udostępnienie go do dalszej alokacji. Jeśli ptr jest pustym wskaźnikiem, nie następuje żadna akcja. W przeciwnym razie, jeśli argument nie jest zgodny ze wskaźnikiem wcześniej zwróconym przez funkcję calloc, malloc lub realloc lub jeśli przestrzeń została zwolniona przez wywołanie funkcji free lub realloc, zachowanie jest niezdefiniowane.

„niezdefiniowane zachowanie” to prawie zawsze awaria programu. Aby tego uniknąć, można bezpiecznie zresetować wskaźnik do NULL. Funkcja free () nie może tego zrobić, ponieważ jest przekazywana tylko wskaźnik, a nie wskaźnik do wskaźnika. Możesz także napisać bezpieczniejszą wersję free (), która zeruje wskaźnik:

void safe_free(void** ptr)
{
  free(*ptr);
  *ptr = NULL;
}
Vijay Mathew
źródło
@DrPizza - Błąd (moim zdaniem) to coś, co powoduje, że Twój program nie działa tak, jak powinien. Jeśli ukryte podwójne darmowe zepsuje twój program, jest to błąd. Jeśli działa dokładnie tak, jak powinien, nie jest to błąd.
Chris Lutz
@DrPizza: Właśnie znalazłem argument, dlaczego należy go ustawić, NULLaby uniknąć maskowania błędów. stackoverflow.com/questions/1025589/… Wygląda na to, że w obu przypadkach niektóre błędy zostały ukryte.
Georg Schölly
1
Pamiętaj, że void pointer-to-pointer ma swoje problemy: c-faq.com/ptrs/genericpp.html
Secure
3
@Chris, nie, najlepszym podejściem jest struktura kodu. Nie rzucaj przypadkowymi mallocami i zwolnij całą bazę kodu, trzymaj powiązane rzeczy razem. „Moduł”, który przydziela zasób (pamięć, plik,…), jest odpowiedzialny za jego zwolnienie i musi udostępniać w tym celu funkcję, która będzie również dbać o wskaźniki. W przypadku dowolnego zasobu masz wtedy dokładnie jedno miejsce, w którym jest on przydzielony, i jedno miejsce, w którym jest on uwalniany, oba blisko siebie.
Zabezpieczony
4
@Chris Lutz: Hogwash. Jeśli napiszesz kod dwukrotnie zwalniający ten sam wskaźnik, w programie wystąpi błąd logiczny. Maskowanie tego błędu logicznego poprzez zapobieganie awarii nie oznacza, że ​​program jest poprawny: nadal robi coś bezsensownego. Nie ma scenariusza, w którym pisanie podwójnego free jest uzasadnione.
DrPizza
4

Uważam, że jest to niewielka pomoc, ponieważ z mojego doświadczenia wynika, że ​​kiedy ludzie uzyskują dostęp do zwolnionej alokacji pamięci, prawie zawsze dzieje się tak, ponieważ mają gdzieś inny wskaźnik do tego. A potem jest to sprzeczne z innym osobistym standardem kodowania, który brzmi „Unikaj niepotrzebnego bałaganu”, więc nie robię tego, ponieważ myślę, że rzadko pomaga i sprawia, że ​​kod jest nieco mniej czytelny.

Jednak - nie ustawię zmiennej na null, jeśli wskaźnik nie ma być ponownie użyty, ale często projekt wyższego poziomu daje mi powód, aby i tak ustawić ją na null. Na przykład jeśli wskaźnik jest członkiem klasy i usunąłem to, na co wskazuje, wówczas "kontrakt", jeśli podoba ci się klasa, jest taki, że ten element członkowski wskaże coś ważnego w dowolnym momencie, więc musi być ustawiony na null z tego powodu. Małe rozróżnienie, ale myślę, że ważne.

W języku c ++ ważne jest, aby zawsze myśleć, kto jest właścicielem tych danych, gdy przydzielasz trochę pamięci (chyba że używasz inteligentnych wskaźników, ale nawet wtedy wymagana jest pewna myśl). Ten proces zwykle prowadzi do tego, że wskaźniki na ogół należą do jakiejś klasy i generalnie chcesz, aby klasa była w stanie przez cały czas, a najłatwiejszym sposobem jest ustawienie zmiennej składowej na NULL, aby wskazać, że wskazuje do niczego teraz.

Wspólny wzór jest ustawienie wszystkich wskazówek państw NULL w konstruktorze i mieć wywołanie destruktora usuwania na wszelkie wskaźniki do danych, że projekt mówi, że klasa jest właścicielem . Oczywiście w tym przypadku musisz ustawić wskaźnik na NULL, gdy usuniesz coś, aby wskazać, że wcześniej nie jesteś właścicielem żadnych danych.

Podsumowując, tak, często ustawiam wskaźnik na NULL po usunięciu czegoś, ale jest to część większego projektu i przemyśleń na temat tego, kto jest właścicielem danych, a nie ślepo przestrzeganie standardowej reguły kodowania. Nie zrobiłbym tego na twoim przykładzie, ponieważ myślę, że nie ma z tego żadnej korzyści i dodaje to „bałaganu”, który z mojego doświadczenia jest tak samo odpowiedzialny za błędy i zły kod, jak tego typu rzeczy.

jcoder
źródło
4

Ostatnio natknąłem się na to samo pytanie po tym, jak szukałem odpowiedzi. Doszedłem do takiego wniosku:

Jest to najlepsza praktyka i należy ją stosować, aby była przenośna we wszystkich (wbudowanych) systemach.

free()jest funkcją biblioteczną, która zmienia się wraz ze zmianą platformy, więc nie należy oczekiwać, że po przekazaniu wskaźnika do tej funkcji i zwolnieniu pamięci, wskaźnik ten zostanie ustawiony na NULL. Może tak nie być w przypadku niektórych bibliotek zaimplementowanych na platformę.

więc zawsze idź

free(ptr);
ptr = NULL;
Jalindar
źródło
3

Ta reguła jest przydatna, gdy próbujesz uniknąć następujących scenariuszy:

1) Masz naprawdę długą funkcję ze skomplikowaną logiką i zarządzaniem pamięcią i nie chcesz przypadkowo ponownie użyć wskaźnika do usuniętej pamięci w dalszej części funkcji.

2) Wskaźnik jest zmienną składową klasy, która ma dość złożone zachowanie i nie chcesz przypadkowo ponownie użyć wskaźnika do usuniętej pamięci w innych funkcjach.

W twoim scenariuszu nie ma to większego sensu, ale jeśli funkcja miałaby się wydłużyć, może to mieć znaczenie.

Możesz argumentować, że ustawienie go na NULL może faktycznie maskować później błędy logiczne lub w przypadku, gdy zakładasz, że jest poprawne, nadal zawieszasz się na NULL, więc nie ma to znaczenia.

Ogólnie radziłbym ustawić go na NULL, gdy uważasz, że to dobry pomysł, i nie przejmować się, gdy uważasz, że nie jest tego wart. Zamiast tego skup się na pisaniu krótkich funkcji i dobrze zaprojektowanych klas.

i_am_jorf
źródło
2

Aby dodać do tego, co powiedzieli inni, jedną dobrą metodą użycia wskaźnika jest zawsze sprawdzanie, czy jest to prawidłowy wskaźnik, czy nie. Coś jak:


if(ptr)
   ptr->CallSomeMethod();

Jawne oznaczenie wskaźnika jako NULL po zwolnieniu pozwala na tego rodzaju użycie w C / C ++.

Aamir
źródło
5
W wielu przypadkach, gdy wskaźnik NULL nie ma sensu, zamiast tego lepiej byłoby napisać asercję.
Erich Kitzmueller
2

To może być bardziej argument do zainicjowania wszystkich wskaźników na NULL, ale coś takiego może być bardzo podstępnym błędem:

void other_func() {
  int *p; // forgot to initialize
  // some unrelated mallocs and stuff
  // ...
  if (p) {
    *p = 1; // hm...
  }
}

void caller() {
  some_func();
  other_func();
}

pkończy się w tym samym miejscu na stosie, co poprzedni nPtr, więc nadal może zawierać pozornie prawidłowy wskaźnik. Przypisanie do *pmoże nadpisać wszelkiego rodzaju niepowiązane rzeczy i prowadzić do brzydkich błędów. Zwłaszcza jeśli kompilator inicjuje zmienne lokalne z zerem w trybie debugowania, ale nie po włączeniu optymalizacji. Więc kompilacje debugowania nie wykazują żadnych oznak błędu, podczas gdy kompilacje wydań wybuchają losowo ...

sth
źródło
2

Ustawienie wskaźnika, który właśnie został zwolniony, na NULL nie jest obowiązkowe, ale jest dobrą praktyką. W ten sposób można uniknąć 1) używania uwolnionego szpicu 2) uwolnienia go

pierrotlefou
źródło
2

Ustawienie wskaźnika na NULL ma na celu ochronę przed tzw. Double-free - sytuacja, w której funkcja free () jest wywoływana więcej niż jeden raz dla tego samego adresu bez ponownego przydzielania bloku pod tym adresem.

Podwójne zwolnienie prowadzi do niezdefiniowanego zachowania - zwykle powoduje uszkodzenie sterty lub natychmiastową awarię programu. Wywołanie free () dla wskaźnika NULL nic nie robi i dlatego jest gwarantowane, że jest bezpieczne.

Więc najlepszą praktyką, chyba że teraz jesteś pewien, że wskaźnik opuści zasięg natychmiast lub wkrótce po funkcji free (), jest ustawienie tego wskaźnika na NULL, aby nawet jeśli funkcja free () zostanie ponownie wywołana, zostanie teraz wywołana dla wskaźnika NULL i niezdefiniowanego zachowania jest omijany.

ostry
źródło
2

Chodzi o to, że jeśli spróbujesz wyłuskać nieaktualny wskaźnik po jego zwolnieniu, chcesz zakończyć się niepowodzeniem (segfault), a nie cicho i tajemniczo.

Ale bądź ostrożny. Nie wszystkie systemy powodują segfault, jeśli wyłuskujesz wartość NULL. W (przynajmniej niektórych wersjach) AIX, * (int *) 0 == 0, a Solaris ma opcjonalną zgodność z tą "funkcją" systemu AIX.

Jaap Weel
źródło
2

Do pierwotnego pytania: ustawienie wskaźnika na NULL bezpośrednio po zwolnieniu zawartości jest całkowitą stratą czasu, pod warunkiem, że kod spełnia wszystkie wymagania, jest w pełni debugowany i nigdy nie zostanie ponownie zmodyfikowany. Z drugiej strony defensywne NULLowanie zwolnionego wskaźnika może być całkiem przydatne, gdy ktoś bezmyślnie doda nowy blok kodu pod free (), gdy projekt oryginalnego modułu nie jest poprawny, a w takim przypadku -kompiluje-ale-nie-robi-czego-chcę-bugi.

W każdym systemie istnieje nieosiągalny cel, jakim jest uczynienie go najłatwiejszym do wykonania właściwej rzeczy, oraz nieredukowalny koszt niedokładnych pomiarów. W C oferujemy zestaw bardzo ostrych, bardzo mocnych narzędzi, które mogą stworzyć wiele rzeczy w rękach wykwalifikowanego pracownika i zadać różnego rodzaju metaforyczne obrażenia, gdy są niewłaściwie obsługiwane. Niektóre są trudne do zrozumienia lub prawidłowego użycia. A ludzie, mając naturalną awersję do ryzyka, robią irracjonalne rzeczy, takie jak sprawdzanie wskaźnika dla wartości NULL, zanim zadzwonią za darmo…

Problem z pomiarem polega na tym, że ilekroć próbujesz oddzielić dobre od gorszych, im bardziej złożony przypadek, tym większe prawdopodobieństwo, że otrzymasz niejednoznaczny pomiar. Jeśli celem jest trzymanie się tylko dobrych praktyk, to niektóre niejednoznaczne zostają wyrzucone z faktycznie złymi. JEŚLI Twoim celem jest wyeliminowanie tego, co złe, to niejasności mogą pozostać przy dobru. Te dwa cele, zachowaj tylko dobre lub wyeliminuj wyraźnie złe, wydają się być diametralnie przeciwstawne, ale zwykle istnieje trzecia grupa, która nie jest ani jednym, ani drugim, niektóre z obu.

Zanim przedstawisz sprawę z działem jakości, spróbuj przejrzeć bazę danych błędów, aby zobaczyć, jak często, jeśli w ogóle, nieprawidłowe wartości wskaźników powodowały problemy, które trzeba było zapisać. Jeśli chcesz coś zmienić, zidentyfikuj najczęstszy problem w swoim kodzie produkcyjnym i zaproponuj trzy sposoby, aby temu zapobiec

Bill IV
źródło
Dobra odpowiedź. Chciałbym dodać jedną rzecz. Przeglądanie bazy danych błędów jest dobre z wielu powodów. Ale w kontekście pierwotnego pytania należy pamiętać, że trudno byłoby stwierdzić, ilu problemów z nieprawidłowymi wskaźnikami udało się zapobiec lub przynajmniej wykryć tak wcześnie, aby nie trafiły one do bazy danych błędów. Historia błędów dostarcza lepszych dowodów na dodanie reguł kodowania.
jimhark,
2

Istnieją dwa powody:

Unikaj awarii podczas podwójnego zwalniania

Napisane przez RageZ w zduplikowanym pytaniu .

Najczęstszym błędem w c jest podwójne wolne. Zasadniczo robisz coś takiego

free(foobar);
/* lot of code */
free(foobar);

i kończy się dość źle, system operacyjny próbuje uwolnić część już zwolnionej pamięci i generalnie powoduje awarię. Więc dobrą praktyką jest ustawienie na NULL, abyś mógł wykonać test i sprawdzić, czy naprawdę potrzebujesz zwolnić tę pamięć

if(foobar != NULL){
  free(foobar);
}

należy również zauważyć, że free(NULL) nic nie da, więc nie musisz pisać instrukcji if. Nie jestem guru systemów operacyjnych, ale nawet teraz jestem ładna większość systemów operacyjnych na podwójnej wolności.

Jest to również główny powód, dla którego wszystkie języki z wyrzucaniem elementów bezużytecznych (Java, dotnet) były tak dumne, że nie mają tego problemu, a także nie muszą pozostawiać programistom zarządzania pamięcią jako całości.

Unikaj używania już zwolnionych wskaźników

Napisane przez Martina v. Löwisa w innej odpowiedzi .

Ustawienie nieużywanych wskaźników na NULL jest stylem obronnym, chroniącym przed wiszącymi błędami wskaźnika. W przypadku uzyskania dostępu do wiszącego wskaźnika po jego zwolnieniu można odczytać lub nadpisać pamięć losową. W przypadku uzyskania dostępu do pustego wskaźnika w większości systemów występuje natychmiastowa awaria, informując od razu o błędzie.

W przypadku zmiennych lokalnych może to być trochę bezcelowe, jeśli jest „oczywiste”, że wskaźnik nie jest już używany po zwolnieniu, więc ten styl jest bardziej odpowiedni dla danych składowych i zmiennych globalnych. Nawet w przypadku zmiennych lokalnych dobrym podejściem może być kontynuacja funkcji po zwolnieniu pamięci.

Aby ukończyć styl, powinieneś również zainicjować wskaźniki na NULL, zanim zostaną przypisane im prawdziwa wartość wskaźnika.

Georg Schölly
źródło
1

Ponieważ masz na miejscu zespół ds. Zapewnienia jakości, pozwolę sobie dodać drobną kwestię dotyczącą kontroli jakości. Niektóre zautomatyzowane narzędzia kontroli jakości dla C oznaczają przypisania do zwolnionych wskaźników jako „bezużyteczne przypisanie do ptr”. Na przykład mówi PC-lint / FlexeLint z Gimpel Software tst.c 8 Warning 438: Last value assigned to variable 'nPtr' (defined at line 5) not used

Istnieją sposoby na selektywne pomijanie komunikatów, więc nadal możesz spełnić oba wymagania dotyczące kontroli jakości, jeśli Twój zespół tak zdecyduje.

Jens
źródło
1

Zawsze dobrze jest zadeklarować zmienną wskaźnikową z wartością NULL, taką jak,

int *ptr = NULL;

Powiedzmy, że ptr wskazuje na adres pamięci 0x1000 . Po użyciu free(ptr)zawsze zaleca się anulowanie zmiennej wskaźnika przez ponowne zadeklarowanie jej na NULL . na przykład:

free(ptr);
ptr = NULL;

Jeśli nie zostanie ponownie zadeklarowana na NULL , zmienna wskaźnikowa nadal wskazuje ten sam adres ( 0x1000 ), ta zmienna wskaźnikowa jest nazywana wskaźnikiem wiszącym . Jeśli zdefiniujesz inną zmienną wskaźnika (powiedzmy, q ) i dynamicznie przydzielisz adres nowemu wskaźnikowi, istnieje szansa, że ​​ten sam adres ( 0x1000 ) zostanie przyjęty przez nową zmienną wskaźnika. Jeśli w przypadku, użyjesz tego samego wskaźnika ( ptr ) i zaktualizujesz wartość pod adresem wskazywanym przez ten sam wskaźnik ( ptr ), to program zakończy zapisywanie wartości w miejscu, na które wskazuje q (ponieważ p i q są wskazujące na ten sam adres (0x1000 )).

na przykład

*ptr = 20; //Points to 0x1000
free(ptr);
int *q = (int *)malloc(sizeof(int) * 2); //Points to 0x1000
*ptr = 30; //Since ptr and q are pointing to the same address, so the value of the address to which q is pointing would also change.
Pankaj Kumar Thapa
źródło
1

Krótko mówiąc: nie chcesz przypadkowo (przez pomyłkę) uzyskać dostępu do uwolnionego adresu. Ponieważ zwalniając adres, zezwalasz na przydzielenie tego adresu w stercie do innej aplikacji.

Jeśli jednak nie ustawisz wskaźnika na NULL i przez pomyłkę spróbuj usunąć odniesienie do wskaźnika lub zmienić wartość tego adresu; WCIĄŻ MOŻESZ TO ZROBIĆ. ALE NIE CZEGO CHCIAŁBYŚ ZROBIĆ.

Dlaczego nadal mogę uzyskać dostęp do zwolnionej lokalizacji pamięci? Ponieważ: Być może zwolniono pamięć, ale zmienna wskaźnika nadal zawierała informacje o adresie pamięci sterty. Tak więc, jako strategia obronna, ustaw ją na NULL.

Ehsan
źródło