Zacznę od stwierdzenia, używaj inteligentnych wskaźników, a nigdy nie będziesz musiał się tym martwić.
Jakie są problemy z następującym kodem?
Foo * p = new Foo;
// (use p)
delete p;
p = NULL;
Zostało to wywołane odpowiedzią i komentarzami na inne pytanie. Jeden komentarz Neila Butterwortha wygenerował kilka pozytywnych głosów:
Ustawienie wskaźników na NULL po usunięciu nie jest uniwersalną dobrą praktyką w C ++. Są chwile, kiedy jest to dobre, i takie, kiedy jest to bezcelowe i może ukryć błędy.
Jest wiele okoliczności, w których to nie pomoże. Ale z mojego doświadczenia wynika, że to nie boli. Niech ktoś mnie oświeci.
c++
pointers
null
dynamic-allocation
Mark Okup
źródło
źródło
delete
wskaźnika zerowego, co jest jednym z powodów, dla których zerowanie wskaźnika może być dobre.Odpowiedzi:
Ustawienie wskaźnika na 0 (które jest „null” w standardowym C ++, definicja NULL z C jest nieco inna) pozwala uniknąć awarii podczas podwójnego usuwania.
Rozważ następujące:
Natomiast:
Innymi słowy, jeśli nie ustawisz usuniętych wskaźników na 0, będziesz miał kłopoty, jeśli wykonujesz podwójne usuwanie. Argumentem przeciwko ustawianiu wskaźników na 0 po usunięciu byłoby to, że po prostu maskuje podwójne błędy usuwania i pozostawia je nieobsługiwane.
Najlepiej oczywiście unikać podwójnych błędów usuwania, ale w zależności od semantyki własności i cykli życia obiektów, może to być trudne do osiągnięcia w praktyce. Wolę zamaskowany błąd podwójnego usuwania od UB.
Na koniec, uwaga dotycząca zarządzania alokacją obiektów, proponuję przyjrzeć się
std::unique_ptr
ścisłej / pojedynczej własności,std::shared_ptr
współdzielonej własności lub innej inteligentnej implementacji wskaźnika, w zależności od potrzeb.źródło
NULL
może maskować błąd podwójnego usuwania. (Niektórzy mogą uznać tę maskę za rozwiązanie - jest, ale niezbyt dobre, ponieważ nie prowadzi do źródła problemu). Jednak ustawienie jej na NULL maskuje daleko (DALEKO!). typowe problemy z dostępem do danych po ich usunięciu.unique_ptr
, co robi to, coauto_ptr
próbowano zrobić.Ustawienie wskaźników na NULL po usunięciu tego, na co wskazywał, z pewnością nie zaszkodzi, ale często jest to trochę pomocna w przypadku bardziej podstawowego problemu: dlaczego w ogóle używasz wskaźnika? Widzę dwa typowe powody:
std::vector
działa i rozwiązuje problem przypadkowego pozostawiania wskaźników do zwolnionej pamięci. Nie ma wskazówek.new
może różnić się od tego, którydelete
jest wywoływany. W międzyczasie wiele obiektów mogło używać tego obiektu jednocześnie. W takim przypadku preferowany byłby wspólny wskaźnik lub coś podobnego.Moja praktyczna zasada jest taka, że jeśli zostawisz wskaźniki w kodzie użytkownika, robisz to źle. W pierwszej kolejności wskaźnik nie powinien wskazywać na śmieci. Dlaczego nie ma przedmiotu, który bierze odpowiedzialność za zapewnienie jego ważności? Dlaczego jego zakres nie kończy się, gdy kończy się wskazany obiekt?
źródło
new
.Mam jeszcze lepszą najlepszą praktykę: tam, gdzie to możliwe, zakończ zakres zmiennej!
źródło
Zawsze ustawiam wskaźnik na
NULL
(teraznullptr
) po usunięciu obiektu (ów), na który wskazuje.Może pomóc wychwycić wiele odwołań do zwolnionej pamięci (zakładając, że twoja platforma zawiera błędy w deref pustego wskaźnika).
Nie przechwytuje wszystkich odniesień do zwolnionej pamięci, jeśli na przykład masz kopie wskaźnika leżące w pobliżu. Ale niektóre są lepsze niż żadne.
Będzie to maskować podwójne usunięcie, ale uważam, że są one znacznie mniej powszechne niż dostęp do już zwolnionej pamięci.
W wielu przypadkach kompilator zamierza go zoptymalizować. Więc argument, że to niepotrzebne, nie przekonuje mnie.
Jeśli już używasz RAII, to nie ma wielu
delete
znaków w twoim kodzie, więc argument, że dodatkowe przypisanie powoduje bałagan, nie przekonuje mnie.Często podczas debugowania wygodnie jest zobaczyć wartość null zamiast nieaktualnego wskaźnika.
Jeśli nadal ci to przeszkadza, użyj inteligentnego wskaźnika lub odwołania.
Ustawiłem również inne typy uchwytów zasobów na wartość bez zasobów, gdy zasób jest wolny (co zwykle występuje tylko w destruktorze opakowania RAII napisanym w celu hermetyzacji zasobu).
Pracowałem nad dużym (9 milionów wypowiedzi) produktem komercyjnym (głównie w C). W pewnym momencie użyliśmy magii makr, aby wyzerować wskaźnik po zwolnieniu pamięci. To natychmiast ujawniło wiele czających się błędów, które zostały szybko naprawione. O ile dobrze pamiętam, nigdy nie mieliśmy podwójnie wolnego błędu.
Aktualizacja: firma Microsoft uważa, że jest to dobra praktyka w zakresie bezpieczeństwa i zaleca ją w swoich zasadach SDL. Najwyraźniej MSVC ++ 11 automatycznie stompuje usunięty wskaźnik (w wielu przypadkach), jeśli kompilujesz z opcją / SDL.
źródło
Po pierwsze, istnieje wiele istniejących pytań dotyczących tego i blisko pokrewnych tematów, na przykład Dlaczego nie można usunąć, ustawić wskaźnik na NULL? .
W twoim kodzie problem tego, co się dzieje (użyj p). Na przykład, jeśli gdzieś masz taki kod:
wtedy ustawienie p na NULL daje bardzo niewiele, ponieważ nadal musisz się martwić o wskaźnik p2.
Nie oznacza to, że ustawienie wskaźnika na NULL jest zawsze bezcelowe. Na przykład, jeśli p była zmienną składową wskazującą na zasób, którego okres istnienia nie był dokładnie taki sam jak klasa zawierająca p, wówczas ustawienie p na NULL mogłoby być użytecznym sposobem wskazania obecności lub braku zasobu.
źródło
Jeśli po
delete
znaku, tak. Gdy wskaźnik zostanie usunięty w konstruktorze lub na końcu metody lub funkcji, nie.Celem tej przypowieści jest przypomnienie programiście w czasie wykonywania, że obiekt został już usunięty.
Jeszcze lepszą praktyką jest użycie inteligentnych wskaźników (współdzielonych lub z zakresem), które automagicznie usuwają obiekty docelowe.
źródło
Jak powiedzieli inni,
delete ptr; ptr = 0;
nie spowoduje, że demony wylecą z twojego nosa. Jednak zachęca do używaniaptr
jako pewnego rodzaju flagi. Kod zostaje zaśmieconydelete
i ustawia wskaźnik naNULL
. Następnym krokiem jest rozproszenieif (arg == NULL) return;
kodu w celu ochrony przed przypadkowym użyciemNULL
wskaźnika. Problem pojawia się, gdy testy przeciwkoNULL
stają się podstawowym sposobem sprawdzania stanu obiektu lub programu.Jestem pewien, że gdzieś jest zapach kodu dotyczący używania wskaźnika jako flagi, ale go nie znalazłem.
źródło
NULL
nie jest to poprawna wartość, prawdopodobnie powinieneś zamiast tego użyć odwołania.Zmienię nieznacznie twoje pytanie:
Istnieją dwa scenariusze, w których można pominąć ustawienie wskaźnika na NULL:
Tymczasem argumentowanie, że ustawienie wskaźnika na NULL może ukryć błędy, brzmi dla mnie jak argument, że nie należy naprawiać błędu, ponieważ poprawka może ukryć inny błąd. Jedynymi błędami, które mogą się pojawić, jeśli wskaźnik nie jest ustawiony na NULL, byłyby te, które próbują użyć wskaźnika. Ale ustawienie go na NULL spowodowałoby dokładnie ten sam błąd, który pojawiłby się, gdybyś używał go ze zwolnioną pamięcią, prawda?
źródło
Jeśli nie masz innego ograniczenia, które zmusza cię do ustawiania lub nie ustawiania wskaźnika na NULL po jego usunięciu (o jednym z takich ograniczeń wspomniał Neil Butterworth ), to moim osobistym upodobaniem jest pozostawienie go tak.
Dla mnie pytanie nie brzmi „czy to dobry pomysł?” ale "jakiemu zachowaniu bym zapobiegał lub pozwolił by odnieść sukces, robiąc to?" Na przykład, jeśli pozwala to innemu kodowi zobaczyć, że wskaźnik nie jest już dostępny, dlaczego inny kod nawet próbuje spojrzeć na zwolnione wskaźniki po ich zwolnieniu? Zwykle jest to błąd.
Wykonuje również więcej pracy niż to konieczne, a także utrudnia debugowanie pośmiertne. Im rzadziej dotykasz pamięci, gdy jej nie potrzebujesz, tym łatwiej jest dowiedzieć się, dlaczego coś się zawiesiło. Wiele razy polegałem na fakcie, że pamięć jest w podobnym stanie, jak wtedy, gdy wystąpił konkretny błąd, aby zdiagnozować i naprawić ten błąd.
źródło
Jawne zerowanie po usunięciu silnie sugeruje czytelnikowi, że wskaźnik reprezentuje coś, co jest koncepcyjnie opcjonalne . Gdybym zobaczył, że to się robi, zacząłbym się martwić, że wszędzie w źródle zostanie użyty wskaźnik, że należy go najpierw przetestować pod kątem NULL.
Jeśli to właśnie masz na myśli, lepiej jest to wyraźnie zaznaczyć w źródle, używając czegoś takiego jak boost :: optional
Ale jeśli naprawdę chciałbyś, żeby ludzie wiedzieli, że wskaźnik „zepsuł się”, będę w 100% zgadzał się z tymi, którzy twierdzą, że najlepiej jest sprawić, by wypadł on poza zakres. Następnie używasz kompilatora, aby zapobiec możliwości złego wyłuskiwania w czasie wykonywania.
To dziecko w kąpieli w C ++, nie powinno go wyrzucać. :)
źródło
W dobrze skonstruowanym programie z odpowiednim sprawdzaniem błędów nie ma powodu, aby nie przypisywać mu wartości null.
0
w tym kontekście występuje samodzielnie jako powszechnie uznawana nieważna wartość. Ciężka porażka i porażka wkrótce.Wiele argumentów przeciwko przypisywaniu
0
sugeruje, że może to ukryć błąd lub skomplikować przepływ sterowania. Zasadniczo jest to albo błąd nadrzędny (nie twoja wina (przepraszam za kiepski kalambur)) albo inny błąd ze strony programisty - być może nawet wskazanie, że przepływ programu stał się zbyt złożony.Jeśli programista chce wprowadzić użycie wskaźnika, który może być zerowy jako wartość specjalna i napisać wszystkie niezbędne uniki wokół tego, jest to komplikacja, którą celowo wprowadził. Im lepsza kwarantanna, tym szybciej znajdziesz przypadki niewłaściwego użycia i tym mniej mogą one rozprzestrzenić się na inne programy.
Aby uniknąć takich przypadków, programy o dobrej strukturze mogą być projektowane przy użyciu funkcji C ++. Możesz użyć odwołań lub po prostu powiedzieć „przekazywanie / używanie pustych lub niepoprawnych argumentów jest błędem” - podejście, które można w równym stopniu zastosować do kontenerów, takich jak inteligentne wskaźniki. Zwiększenie spójności i poprawności zachowania uniemożliwia tym błędom dotarcie daleko.
Stamtąd masz tylko bardzo ograniczony zakres i kontekst, w którym może istnieć pusty wskaźnik (lub jest dozwolony).
To samo można zastosować do wskaźników, które nie są
const
. Podążanie za wartością wskaźnika jest trywialne, ponieważ jego zasięg jest tak mały, a niewłaściwe użycie jest sprawdzane i dobrze zdefiniowane. Jeśli Twój zestaw narzędzi i inżynierowie nie mogą śledzić programu po szybkim przeczytaniu lub występuje niewłaściwe sprawdzanie błędów lub niespójny / łagodny przepływ programu, masz inne, większe problemy.Wreszcie, twój kompilator i środowisko prawdopodobnie mają pewne zabezpieczenia na czas, gdy chcesz wprowadzić błędy (bazgranie), wykryć dostęp do zwolnionej pamięci i złapać inne powiązane UB. Możesz również wprowadzić podobną diagnostykę do swoich programów, często bez wpływu na istniejące programy.
źródło
Pozwól mi rozszerzyć to, co już zawarłeś w swoim pytaniu.
Oto treść pytania w formie punktorów:
Ustawienie wskaźników na NULL po usunięciu nie jest uniwersalną dobrą praktyką w C ++. Są chwile, kiedy:
Jednak nie ma czasu, kiedy to jest złe ! Będzie nie wprowadzać więcej błędów jawnie Nulling go, nie będzie przeciekać pamięć, nie będzie przyczyną zachowanie niezdefiniowane się zdarzyć.
Więc jeśli masz wątpliwości, po prostu anuluj to.
Powiedziawszy to, jeśli czujesz, że musisz jawnie wyzerować jakiś wskaźnik, to wydaje mi się, że nie podzieliłeś metody wystarczająco i powinieneś spojrzeć na podejście refaktoryzacji zwane "metodą wyodrębniania", aby podzielić metodę na oddzielne części.
źródło
Tak.
Jedyną „szkodą”, jaką może wyrządzić, jest wprowadzenie do programu nieefektywności (niepotrzebnej operacji przechowywania) - ale ten narzut będzie w większości przypadków nieistotny w stosunku do kosztu alokowania i zwalniania bloku pamięci.
Jeśli tego nie zrobisz, zrobisz to pewnego dnia miał kilka nieprzyjemnych błędów derefernce wskaźnikowych.
Zawsze używam makra do usuwania:
(i podobnie dla tablicy, free (), zwalnianie uchwytów)
Można także napisać metody „usuwania własnego”, które pobierają odniesienie do wskaźnika kodu wywołującego, więc wymuszają na wskaźniku kodu wywołującego wartość NULL. Na przykład, aby usunąć poddrzewo wielu obiektów:
edytować
Tak, te techniki naruszają niektóre zasady dotyczące używania makr (i tak, w dzisiejszych czasach prawdopodobnie można osiągnąć ten sam rezultat za pomocą szablonów) - ale używając przez wiele lat nigdy nie uzyskałem dostępu do martwej pamięci - jednej z najgorszych i najtrudniejszych i debugowanie problemów, z którymi możesz się zmierzyć, zajmuje najwięcej czasu. W praktyce przez wiele lat skutecznie wyeliminowali całą klasę błędów z każdego zespołu, do którego je wprowadziłem.
Istnieje również wiele sposobów na zaimplementowanie powyższego - po prostu próbuję zilustrować ideę zmuszania ludzi do NULL wskaźnika, jeśli usuwają obiekt, zamiast zapewnić im środki do zwolnienia pamięci, która nie powoduje ZEROWANIA wskaźnika wywołującego .
Oczywiście powyższy przykład to tylko krok w kierunku automatycznego wskaźnika. Czego nie zasugerowałem, ponieważ OP specjalnie pytał o przypadek nie używania automatycznego wskaźnika.
źródło
anObject->Delete(anObject)
unieważnienieanObject
wskaźnika. To po prostu przerażające. Powinieneś stworzyć do tego metodę statyczną, aby przynajmniej był do tego zmuszonyTreeItem::Delete(anObject)
.„Są chwile, kiedy robienie tego jest dobre i takie, kiedy jest to bezcelowe i może ukryć błędy”
Widzę dwa problemy: Ten prosty kod:
staje się for-linerem w środowisku wielowątkowym:
„Najlepsza praktyka” Dona Neufelda nie zawsze ma zastosowanie. Np. W jednym projekcie motoryzacyjnym musieliśmy ustawić wskaźniki na 0 nawet w destruktorach. Mogę sobie wyobrazić, że w oprogramowaniu krytycznym dla bezpieczeństwa takie reguły nie są rzadkością. Łatwiej (i mądrze) jest ich przestrzegać niż próbować przekonać zespół / sprawdzający kod dla każdego wskaźnika używanego w kodzie, że linia zerująca ten wskaźnik jest zbędna.
Innym niebezpieczeństwem jest poleganie na tej technice w kodzie wykorzystującym wyjątki:
W takim kodzie albo tworzysz wyciek zasobów i odkładasz problem, albo proces się zawiesza.
Tak więc te dwa problemy przechodzące spontanicznie przez moją głowę (Herb Sutter z pewnością powiedziałby więcej) sprawiają, że wszystkie pytania typu „Jak uniknąć używania inteligentnych wskazówek i bezpiecznie wykonywać swoją pracę z normalnymi wskazówkami” są przestarzałe.
źródło
Zawsze istnieje problem ze zwisającymi wskaźnikami .
źródło
Jeśli zamierzasz ponownie przydzielić wskaźnik przed jego ponownym użyciem (wyłuskiwanie go, przekazanie do funkcji itp.), Nadanie mu wartości NULL jest tylko dodatkową operacją. Jeśli jednak nie masz pewności, czy zostanie on ponownie przydzielony, czy nie, zanim zostanie ponownie użyty, ustawienie go na NULL jest dobrym pomysłem.
Jak wielu powiedziało, oczywiście znacznie łatwiej jest po prostu używać inteligentnych wskaźników.
Edycja: Jak powiedział Thomas Matthews w tej wcześniejszej odpowiedzi , jeśli wskaźnik zostanie usunięty w destruktorze, nie ma potrzeby przypisywania mu wartości NULL, ponieważ nie zostanie on ponownie użyty, ponieważ obiekt jest już niszczony.
źródło
Mogę sobie wyobrazić ustawienie wskaźnika na NULL po usunięciu go, co jest przydatne w rzadkich przypadkach, gdy istnieje uzasadniony scenariusz ponownego użycia go w pojedynczej funkcji (lub obiekcie). W przeciwnym razie nie ma to sensu - wskaźnik musi wskazywać na coś znaczącego, o ile istnieje - kropka.
źródło
Jeśli kod nie należy do najbardziej krytycznej dla wydajności części aplikacji, zachowaj prostotę i użyj shared_ptr:
Wykonuje zliczanie referencji i jest bezpieczny dla wątków. Możesz go znaleźć w tr1 (przestrzeń nazw std :: tr1, #include <memory>) lub, jeśli Twój kompilator go nie zapewnia, pobierz z boost.
źródło