Czy dobrą praktyką jest NULL wskaźnik po usunięciu go?


150

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.


6
@Andre: Technicznie jest niezdefiniowany. Najprawdopodobniej będziesz mieć dostęp do tej samej pamięci co poprzednio, ale teraz może być używana przez coś innego. Jeśli usuniesz pamięć dwukrotnie, prawdopodobnie spieprzysz wykonanie programu w trudny do znalezienia sposób. Jest to jednak bezpieczne dla deletewskaźnika zerowego, co jest jednym z powodów, dla których zerowanie wskaźnika może być dobre.
David Thornley,

5
@ André Pena, to jest niezdefiniowane. Często nie jest to nawet powtarzalne. Ustawiasz wskaźnik na NULL, aby błąd był bardziej widoczny podczas debugowania i być może, aby był bardziej powtarzalny.
Mark Ransom

3
@ André: Nikt nie wie. Jest to niezdefiniowane zachowanie. Może się zawiesić z naruszeniem zasad dostępu lub może nadpisać pamięć używaną przez resztę aplikacji. Standard językowy nie gwarantuje, co się stanie, dlatego nie możesz ufać aplikacji, gdy to się stanie. To mógł być zwolniony pocisków nuklearnych lub formatowania dysku twardym. może zepsuć pamięć aplikacji lub sprawić, że demony wylecą ci z nosa. Wszystkie zakłady są wyłączone.
jalf

16
Latające demony to funkcja, a nie błąd.
jball

3
To pytanie nie jest powtórzeniem, ponieważ drugie pytanie dotyczy C, a to C ++. Wiele odpowiedzi zależy od takich rzeczy, jak inteligentne wskaźniki, które nie są dostępne w C ++.
Adrian McCarthy

Odpowiedzi:


86

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:

Foo* foo = 0; // Sets the pointer to 0 (C++ NULL)
delete foo; // Won't do anything

Natomiast:

Foo* foo = new Foo();
delete foo; // Deletes the object
delete foo; // Undefined behavior 

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_ptrwspółdzielonej własności lub innej inteligentnej implementacji wskaźnika, w zależności od potrzeb.


13
Twoja aplikacja nie zawsze ulegnie awarii przy podwójnym usunięciu. W zależności od tego, co dzieje się między dwoma usunięciami, wszystko może się zdarzyć. Najprawdopodobniej uszkodzisz swoją stertę i w pewnym momencie ulegniesz awarii w zupełnie niezwiązanym z nią fragmencie kodu. Podczas gdy segfault jest zwykle lepszy niż ciche ignorowanie błędu, segfault nie jest w tym przypadku gwarantowany i ma wątpliwą użyteczność.
Adam Rosenfield

28
Problem polega na tym, że masz podwójne usunięcie. Ustawienie wskaźnika na NULL po prostu ukrywa ten fakt, nie poprawia go ani nie czyni go bezpieczniejszym. Wyobraź sobie, że mainainer wraca rok później i widzi, że foo zostało usunięte. Teraz uważa, że ​​może ponownie użyć wskaźnika, niestety może przegapić drugie usunięcie (może nawet nie być w tej samej funkcji), a teraz ponowne użycie wskaźnika jest teraz usuwane do kosza przy drugim usunięciu. Każdy dostęp po drugim usunięciu jest teraz poważnym problemem.
Martin York,

11
To prawda, że ​​ustawienie wskaźnika NULLmoż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.
Adrian McCarthy

AFAIK, std :: auto_ptr został wycofany w nadchodzącym standardzie C ++
rafak

Nie powiedziałbym, że jest przestarzały, to brzmi tak, jakby pomysł po prostu zniknął. Zamiast tego jest zastępowany semantyką ruchu unique_ptr, co robi to, co auto_ptrpróbowano zrobić.
GManNickG

55

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:

  • Po prostu chciałeś, żeby coś zostało przydzielone na stosie. W takim przypadku owinięcie go w obiekt RAII byłoby znacznie bezpieczniejsze i czystsze. Zakończyć zasięg obiektu RAII, gdy obiekt już nie jest potrzebny. Tak właśnie std::vectordziała i rozwiązuje problem przypadkowego pozostawiania wskaźników do zwolnionej pamięci. Nie ma wskazówek.
  • A może chciałeś jakiejś złożonej semantyki współwłasności. Wskaźnik zwrócony z newmoże różnić się od tego, który deletejest 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?


15
Więc argumentujesz, że w ogóle nie powinno być surowego wskaźnika, a wszystko, co dotyczy tego wskaźnika, nie powinno być pobłogosławione terminem „dobra praktyka”? Słusznie.
Mark Ransom

7
Cóż, mniej więcej. Nie powiedziałbym, że niczego , co dotyczy surowego wskaźnika, nie można nazwać dobrą praktyką. Tylko tyle, że to raczej wyjątek niż reguła. Zwykle obecność wskaźnika jest wskaźnikiem, że coś jest nie tak na głębszym poziomie.
jalf

3
ale odpowiadając na bezpośrednie pytanie, nie, nie widzę, w jaki sposób ustawienie wskaźników na wartość null może kiedykolwiek spowodować błędy.
jalf

7
Nie zgadzam się - zdarzają się przypadki, gdy wskaźnik jest dobry w użyciu. Na przykład na stosie znajdują się 2 zmienne i chcesz wybrać jedną z nich. Lub chcesz przekazać opcjonalną zmienną do funkcji. Powiedziałbym, że nigdy nie należy używać surowego wskaźnika w połączeniu z new.
rlbond

4
kiedy wskaźnik wyszedł poza zakres, nie widzę, jak cokolwiek lub ktokolwiek mógłby mieć z tym problem.
jalf

43

Mam jeszcze lepszą najlepszą praktykę: tam, gdzie to możliwe, zakończ zakres zmiennej!

{
    Foo* pFoo = new Foo;
    // use pFoo
    delete pFoo;
}

17
Tak, RAII to twój przyjaciel. Owiń go w klasę, a stanie się to jeszcze prostsze. Lub w ogóle nie zajmuj się pamięcią za pomocą STL!
Brian

24
Tak, rzeczywiście, to najlepsza opcja. Nie odpowiada jednak na pytanie.
Mark Ransom

3
Wydaje się, że jest to tylko produkt uboczny używania okresów zakresów funkcji i tak naprawdę nie rozwiązuje problemu. Kiedy używasz wskaźników, zwykle przekazujesz ich kopie na kilka warstw w głąb, a wtedy twoja metoda jest naprawdę bez znaczenia w celu rozwiązania problemu. Chociaż zgadzam się, że dobry projekt pomoże ci wyodrębnić błędy, nie sądzę, że twoja metoda jest podstawowym środkiem do tego celu.
San Jacinto

2
Pomyśl o tym, jeśli możesz to zrobić, dlaczego po prostu nie zapomnisz o stosie i nie wyciągniesz całej swojej pamięci ze stosu?
San Jacinto,

4
Mój przykład jest celowo minimalny. Na przykład, zamiast nowego, być może obiekt jest tworzony przez fabrykę, w takim przypadku nie może trafić na stos. A może nie jest utworzony na początku zakresu, ale znajduje się w jakiejś strukturze. To, co ilustruję, to to, że to podejście wykryje każde niewłaściwe użycie wskaźnika w czasie kompilacji , podczas gdy NULL znajdzie wszelkie niewłaściwe użycie w czasie wykonywania .
Don Neufeld

31

Zawsze ustawiam wskaźnik na NULL(teraz nullptr) po usunięciu obiektu (ów), na który wskazuje.

  1. 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).

  2. 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.

  3. 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.

  4. W wielu przypadkach kompilator zamierza go zoptymalizować. Więc argument, że to niepotrzebne, nie przekonuje mnie.

  5. Jeśli już używasz RAII, to nie ma wielu deleteznaków w twoim kodzie, więc argument, że dodatkowe przypisanie powoduje bałagan, nie przekonuje mnie.

  6. Często podczas debugowania wygodnie jest zobaczyć wartość null zamiast nieaktualnego wskaźnika.

  7. 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.


12

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:

Foo * p2 = p;

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.


1
Zgadzam się, że są chwile, kiedy to nie pomoże, ale zdawałeś się sugerować, że może to być aktywnie szkodliwe. Czy taki był twój zamiar, czy źle to odczytałem?
Mark Ransom

1
To, czy istnieje kopia wskaźnika, nie ma znaczenia dla pytania, czy zmienna wskaźnika powinna być ustawiona na NULL. Ustawienie go na NULL jest dobrą praktyką z tych samych względów, że czyszczenie naczyń po kolacji jest dobrą praktyką - chociaż nie jest to zabezpieczenie przed wszystkimi błędami, które może mieć kod, promuje dobrą kondycję kodu.
Franci Penov

2
@Franci Wiele osób się z tobą nie zgadza. To, czy istnieje kopia, z pewnością ma znaczenie, jeśli spróbujesz użyć kopii po usunięciu oryginału.

3
Franci, jest różnica. Czyścisz naczynia, ponieważ używasz ich ponownie. Po usunięciu wskaźnika nie potrzebujesz. To powinna być ostatnia rzecz, jaką robisz. Lepszą praktyką jest całkowite uniknięcie tej sytuacji.
GManNickG,

1
Możesz ponownie użyć zmiennej, ale wtedy nie będzie to już przypadek programowania obronnego; tak zaprojektowałeś rozwiązanie danego problemu. OP dyskutuje, czy ten styl obronny jest czymś, do czego powinniśmy dążyć, a nie czy nigdy nie ustawimy wskaźnika na null. I idealnie, jeśli chodzi o twoje pytanie, tak! Nie używaj wskaźników po ich usunięciu!
GManNickG,

7

Jeśli po deleteznaku, 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.


Wszyscy (łącznie z pierwotnym pytającym) zgadzają się, że mądre wskazówki są najlepszym rozwiązaniem. Kod ewoluuje. Po usunięciu może nie być więcej kodu, gdy po raz pierwszy go poprawisz, ale prawdopodobnie zmieni się to z czasem. Przypisanie zadania pomaga, kiedy to się dzieje (i w międzyczasie prawie nic nie kosztuje).
Adrian McCarthy

3

Jak powiedzieli inni, delete ptr; ptr = 0;nie spowoduje, że demony wylecą z twojego nosa. Jednak zachęca do używania ptrjako pewnego rodzaju flagi. Kod zostaje zaśmiecony deletei ustawia wskaźnik na NULL. Następnym krokiem jest rozproszenie if (arg == NULL) return;kodu w celu ochrony przed przypadkowym użyciem NULLwskaźnika. Problem pojawia się, gdy testy przeciwko NULLstają 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.


9
Nie ma nic złego w używaniu wskaźnika jako flagi. Jeśli używasz wskaźnika i NULLnie jest to poprawna wartość, prawdopodobnie powinieneś zamiast tego użyć odwołania.
Adrian McCarthy,

2

Zmienię nieznacznie twoje pytanie:

Czy użyłbyś niezainicjowanego wskaźnika? Wiesz, taki, którego nie ustawiłeś na NULL lub nie przydzieliłeś pamięci, na którą wskazuje?

Istnieją dwa scenariusze, w których można pominąć ustawienie wskaźnika na NULL:

  • zmienna wskaźnikowa natychmiast wychodzi poza zakres
  • przeładowałeś semantykę wskaźnika i używasz jego wartości nie tylko jako wskaźnika pamięci, ale także jako klucza lub wartości surowej. to podejście ma jednak inne problemy.

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?


(A) "brzmi jak argument, że nie powinieneś naprawiać błędu" Brak ustawienia wskaźnika na NULL nie jest błędem. (B) „Ale ustawienie wartości NULL spowodowałoby w rzeczywistości dokładnie ten sam błąd” Nie. Ustawienie wartości NULL ukrywa podwójne usuwanie . (C) Podsumowanie: Ustawienie wartości NULL ukrywa podwójne usuwanie, ale uwidacznia nieaktualne odwołania. Brak ustawienia na NULL może ukryć nieaktualne odwołania, ale ujawnia podwójne usunięcia. Obie strony zgadzają się, że prawdziwym problemem jest naprawienie przestarzałych odniesień i podwójne usuwanie.
Mooing Duck,

2

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.


2

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

optional<Foo*> p (new Foo);
// (use p.get(), but must test p for truth first!...)
delete p.get();
p = optional<Foo*>();

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ć. :)


2

W dobrze skonstruowanym programie z odpowiednim sprawdzaniem błędów nie ma powodu, aby nie przypisywać mu wartości null. 0w 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 0sugeruje, ż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.


1

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:

  • dobrze jest zrobić
  • i czasy, kiedy jest to bezcelowe i może ukryć błędy.

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.


Nie zgadzam się z tym, że „nie ma takich momentów, kiedy jest źle”. Zastanów się, ile narzędzi wprowadza ten idiom. Masz nagłówek zawarty w każdej jednostce, która coś usuwa, a wszystkie te lokalizacje usuwania stają się nieco mniej proste.
GManNickG

Są chwile, kiedy jest źle. Jeśli ktoś spróbuje wyłuskać twój wskaźnik usunięty-teraz-zerowy, gdy nie powinien, prawdopodobnie nie ulegnie awarii, a ten błąd jest „ukryty”. Jeśli wyłuskują twój usunięty wskaźnik, który nadal ma jakąś losową wartość, prawdopodobnie zauważysz, a błąd będzie łatwiejszy do zauważenia.
Carson Myers,

@Carson: Moje doświadczenie jest zupełnie odwrotne: wyłuskiwanie nullptr prawie na wszystkie sposoby spowoduje awarię aplikacji i może zostać przechwycone przez debugger. Wycofywanie odwołań do wiszącego wskaźnika zwykle nie powoduje od razu problemu, ale często prowadzi do niepoprawnych wyników lub innych błędów.
MikeMB

@MikeMB Całkowicie się zgadzam, moje poglądy na ten temat znacznie się zmieniły w ciągu ostatnich ~ 6,5 roku
Carson Myers

Jeśli chodzi o bycie programistą, wszyscy byliśmy kimś innym 6-7 lat temu :) Nie jestem nawet pewien, czy odważyłbym się dziś odpowiedzieć na pytanie C / C ++ :)
Lasse V. Karlsen

1

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:

#define SAFEDELETE(ptr) { delete(ptr); ptr = NULL; }

(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:

static void TreeItem::DeleteSubtree(TreeItem *&rootObject)
{
    if (rootObject == NULL)
        return;

    rootObject->UnlinkFromParent();

    for (int i = 0; i < numChildren)
       DeleteSubtree(rootObject->child[i]);

    delete rootObject;
    rootObject = NULL;
}

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.


2
Makra to zły pomysł, szczególnie gdy wyglądają jak normalne funkcje. Jeśli chcesz to zrobić, użyj funkcji szablonu.

2
Wow ... Nigdy nie widziałem czegoś takiego, jak anObject->Delete(anObject)unieważnienie anObjectwskaźnika. To po prostu przerażające. Powinieneś stworzyć do tego metodę statyczną, aby przynajmniej był do tego zmuszony TreeItem::Delete(anObject).
D.Shawley,

Przepraszamy, wpisałem to jako funkcję, a nie używając odpowiedniej wielkiej litery „to jest makro”. Poprawione. Dodałem również komentarz, aby lepiej się wytłumaczyć.
Jason Williams

I masz rację, mój szybko rozwalony przykład był bzdurą! Naprawiony :-). Po prostu próbowałem wymyślić szybki przykład, aby zilustrować tę ideę: każdy kod, który usuwa wskaźnik, powinien zapewniać, że wskaźnik jest ustawiony na NULL, nawet jeśli ktoś inny (dzwoniący) jest właścicielem tego wskaźnika. Dlatego zawsze przekazuj odniesienie do wskaźnika, aby można było wymusić na nim wartość NULL w miejscu usunięcia.
Jason Williams

1

„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:

delete myObj;
myobj = 0

staje się for-linerem w środowisku wielowątkowym:

lock(myObjMutex); 
delete myObj;
myobj = 0
unlock(myObjMutex);

„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:

try{  
   delete myObj; //exception in destructor
   myObj=0
}
catch
{
   //myObj=0; <- possibly resource-leak
}

if (myObj)
  // use myObj <--undefined behaviour

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.


Nie widzę, jak 4-liniowa jest znacznie bardziej złożona niż 3-liniowa (i tak należy użyć lock_guards) i jeśli twój destruktor rzuca, i tak masz kłopoty.
MikeMB

Kiedy po raz pierwszy zobaczyłem tę odpowiedź, nie rozumiałem, dlaczego chcesz wyzerować wskaźnik w destruktorze, ale teraz tak - dzieje się tak w przypadku, gdy obiekt będący właścicielem wskaźnika jest używany po jego usunięciu!
Mark Ransom


0

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.


0

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.


0

Jeśli kod nie należy do najbardziej krytycznej dla wydajności części aplikacji, zachowaj prostotę i użyj shared_ptr:

shared_ptr<Foo> p(new Foo);
//No more need to call delete

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.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.