wake-up-neo.com

Ist es sinnvoll, nach dem Löschen einen Zeiger auf NULL zu setzen?

Ich beginne mit den Worten: Verwenden Sie intelligente Zeiger, und Sie müssen sich nie darum kümmern.

Was sind die Probleme mit dem folgenden Code?

Foo * p = new Foo;
// (use p)
delete p;
p = NULL;

Dies wurde durch eine Antwort und Kommentare auf eine andere Frage ausgelöst. Ein Kommentar von Neil Butterworth hat ein paar positive Stimmen abgegeben:

Das Setzen von Zeigern auf NULL nach dem Löschen ist in C++ keine allgemein anerkannte Vorgehensweise. Es gibt Zeiten, in denen es gut ist, und Zeiten, in denen es sinnlos ist und Fehler verbergen kann.

Es gibt viele Umstände, unter denen es nicht helfen würde. Aber meiner Erfahrung nach kann es nicht schaden. Jemand klärt mich auf.

136
Mark Ransom

Das Setzen eines Zeigers auf 0 (was in Standard-C++ "null" ist, das Definieren von NULL von C ist etwas anders) vermeidet Abstürze bei doppelten Löschungen.

Folgendes berücksichtigen:

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

Wohingegen:

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

Mit anderen Worten, wenn Sie gelöschte Zeiger nicht auf 0 setzen, treten Probleme auf, wenn Sie doppelte Löschvorgänge ausführen. Ein Argument gegen das Setzen von Zeigern auf 0 nach dem Löschen wäre, dass dadurch nur doppelte Löschfehler maskiert und nicht behandelt werden.

Es ist natürlich am besten, keine doppelten Löschfehler zu haben, aber je nach Besitzersemantik und Objektlebenszyklus kann dies in der Praxis schwierig sein. Ich bevorzuge einen maskierten doppelten Löschfehler gegenüber UB.

Abschließend noch eine Randnotiz zur Verwaltung der Objektzuordnung: Schauen Sie sich doch einmal std::unique_ptr für strenge/singuläre Eigentumsverhältnisse, std::shared_ptr für Shared Ownership oder eine andere Smart Pointer-Implementierung, je nach Ihren Anforderungen.

79
Håvard S

Das Setzen von Zeigern auf NULL, nachdem Sie das gelöscht haben, worauf es zeigt, kann sicherlich nicht schaden, aber es ist oft eine kleine Hilfe bei einem grundlegenderen Problem: Warum verwenden Sie überhaupt einen Zeiger? Ich sehe zwei typische Gründe:

  • Du wolltest einfach etwas auf dem Haufen haben. In diesem Fall wäre das Einwickeln in ein RAII-Objekt viel sicherer und sauberer gewesen. Beenden Sie den Gültigkeitsbereich des RAII-Objekts, wenn Sie das Objekt nicht mehr benötigen. So geht das std::vector funktioniert, und es löst das Problem, dass Zeiger versehentlich dem freigegebenen Speicher überlassen werden. Es gibt keine Hinweise.
  • Oder Sie wollten eine komplexe Semantik der gemeinsamen Eigentümerschaft. Der von new zurückgegebene Zeiger stimmt möglicherweise nicht mit dem Zeiger überein, auf dem delete aufgerufen wird. Möglicherweise haben mehrere Objekte das Objekt in der Zwischenzeit gleichzeitig verwendet. In diesem Fall wäre ein gemeinsamer Zeiger oder ähnliches vorzuziehen gewesen.

Meine Faustregel lautet: Wenn Sie Zeiger im Benutzercode belassen, tun Sie es falsch. Der Zeiger sollte nicht da sein, um überhaupt auf Müll zu zeigen. Warum übernimmt ein Objekt keine Verantwortung für die Gewährleistung seiner Gültigkeit? Warum endet der Gültigkeitsbereich nicht, wenn das Objekt, auf das verwiesen wird, dies tut?

55
jalf

Ich habe eine noch bessere Best Practice: Beenden Sie, wo immer möglich, den Gültigkeitsbereich der Variablen!

{
    Foo* pFoo = new Foo;
    // use pFoo
    delete pFoo;
}
42
Don Neufeld

Ich habe immer einen Zeiger auf NULL (jetzt nullptr) gesetzt, nachdem ich die Objekte gelöscht habe, auf die er zeigt.

  1. Es kann dabei helfen, viele Verweise auf den freigegebenen Speicher abzufangen (vorausgesetzt, Ihre Plattformfehler befinden sich auf einem Deref eines Nullzeigers).

  2. Es werden nicht alle Verweise auf den freien Speicher abgerufen, wenn beispielsweise Kopien des Zeigers herumliegen. Aber manche sind besser als keine.

  3. Es maskiert ein doppeltes Löschen, aber ich finde, dass diese weitaus seltener sind als Zugriffe auf bereits freigegebenen Speicher.

  4. In vielen Fällen wird der Compiler es weg optimieren. Das Argument, dass es unnötig ist, überzeugt mich nicht.

  5. Wenn Sie RAII bereits verwenden, enthält Ihr Code zunächst nicht viele deletes, sodass mich das Argument, dass die zusätzliche Zuweisung zu Unordnung führt, nicht überzeugt.

  6. Beim Debuggen ist es oft praktisch, den Nullwert anstelle eines veralteten Zeigers zu sehen.

  7. Wenn Sie dies immer noch stört, verwenden Sie stattdessen einen intelligenten Zeiger oder eine Referenz.

Ich habe auch andere Arten von Ressourcenhandles auf den Wert no-resource gesetzt, wenn die Ressource freigegeben ist (normalerweise nur im Destruktor eines RAII-Wrappers, der zum Einkapseln der Ressource geschrieben wurde).

Ich habe an einem großen (9 Millionen Kontoauszüge) kommerziellen Produkt gearbeitet (hauptsächlich in C). Zu einem Zeitpunkt haben wir Makromagie verwendet, um den Zeiger auf Null zu setzen, als der Speicher freigegeben wurde. Dies deckte sofort viele lauernde Fehler auf, die umgehend behoben wurden. Soweit ich mich erinnern kann, hatten wir nie einen doppelten Fehler.

Update: Microsoft ist der Ansicht, dass dies eine gute Sicherheitsmaßnahme ist, und empfiehlt diese in den SDL-Richtlinien. Anscheinend wird MSVC++ 11 den gelöschten Zeiger stampfen automatisch (in vielen Fällen), wenn Sie mit der Option/SDL kompilieren.

28
Adrian McCarthy

Erstens gibt es viele Fragen zu diesem und verwandten Themen, z. B. Warum wird der Zeiger beim Löschen nicht auf NULL gesetzt? .

In Ihrem Code das Problem, was los ist (verwenden Sie p). Wenn Sie beispielsweise irgendwo Code wie diesen haben:

Foo * p2 = p;

wenn Sie dann p auf NULL setzen, wird nur sehr wenig erreicht, da Sie immer noch den Zeiger p2 haben, um den Sie sich Sorgen machen müssen.

Dies bedeutet nicht, dass das Setzen eines Zeigers auf NULL immer sinnlos ist. Wenn p beispielsweise eine Mitgliedsvariable wäre, die auf eine Ressource verweist, deren Lebensdauer nicht exakt mit der Klasse übereinstimmt, die p enthält, könnte das Setzen von p auf NULL ein nützlicher Weg sein, um das Vorhandensein oder Fehlen der Ressource anzuzeigen.

12
anon

Wenn nach delete mehr Code steht, Ja. Wenn der Zeiger in einem Konstruktor oder am Ende einer Methode oder Funktion gelöscht wird, wird No.

Der Sinn dieser Parabel ist es, den Programmierer während der Laufzeit daran zu erinnern, dass das Objekt bereits gelöscht wurde.

Eine noch bessere Methode ist die Verwendung von Smart Pointern (gemeinsam oder mit Gültigkeitsbereich), die ihre Zielobjekte automatisch löschen.

7
Thomas Matthews

Wie andere gesagt haben, wird delete ptr; ptr = 0; Nicht dazu führen, dass Dämonen aus deiner Nase fliegen. Es ermutigt jedoch die Verwendung von ptr als eine Art Flagge. Der Code wird übersät mit delete und setzt den Zeiger auf NULL. Der nächste Schritt ist, if (arg == NULL) return; durch Ihren Code zu streuen, um sich gegen die versehentliche Verwendung eines NULL Zeigers zu schützen. Das Problem tritt auf, wenn die Prüfungen mit NULL zu Ihrem primären Mittel für die Prüfung des Status eines Objekts oder Programms werden.

Ich bin sicher, dass es einen Codegeruch gibt, wenn man irgendwo einen Zeiger als Flag verwendet, aber ich habe keinen gefunden.

3
D.Shawley

Wenn Sie keine andere Einschränkung haben, die Sie zwingt, den Zeiger nach dem Löschen entweder auf NULL zu setzen oder nicht (eine solche Einschränkung wurde von Neil Butterworth erwähnt), ist es meine persönliche Präferenz, sie zu lassen .

Für mich ist die Frage nicht "ist das eine gute Idee?" aber "Welches Verhalten würde ich verhindern oder zulassen, damit dies gelingt?" Wenn dies beispielsweise einem anderen Code ermöglicht, festzustellen, dass der Zeiger nicht mehr verfügbar ist, warum versucht ein anderer Code dann überhaupt, freigegebene Zeiger nach ihrer Freigabe zu betrachten? Normalerweise ist es ein Fehler.

Es macht auch mehr Arbeit als nötig und behindert das Post-Mortem-Debugging. Je weniger Sie den Speicher berühren, nachdem Sie ihn nicht mehr benötigen, desto einfacher ist es, herauszufinden, warum etwas abgestürzt ist. Oft habe ich mich darauf verlassen, dass sich der Speicher in einem ähnlichen Zustand befindet wie zu dem Zeitpunkt, als ein bestimmter Fehler aufgetreten ist, um diesen Fehler zu diagnostizieren und zu beheben.

2
MSN

Das explizite Nullen nach dem Löschen weist einen Leser stark darauf hin, dass der Zeiger etwas darstellt, was konzeptionell ist wahlweise. Wenn ich das gesehen habe, würde ich mir Sorgen machen, dass überall in der Quelle der Zeiger verwendet wird, der zuerst gegen NULL getestet werden sollte.

Wenn Sie das tatsächlich meinen, ist es besser, dies in der Quelle mit etwas wie boost :: optional explizit zu machen

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

Aber wenn Sie wirklich wollten, dass die Leute wissen, dass der Zeiger "schlecht geworden" ist, stimme ich zu 100% mit denen überein, die sagen, das Beste, was zu tun ist, ist, ihn außer Reichweite zu bringen. Dann verwenden Sie den Compiler, um die Möglichkeit von fehlerhaften Dereferenzen zur Laufzeit zu verhindern.

Das ist das Baby in all dem C++ - Badewasser, das man nicht rausschmeißen sollte. :)

2
HostileFork

Ich werde Ihre Frage etwas ändern:

Würden Sie einen nicht initialisierten Zeiger verwenden? Wissen Sie, eine, die Sie nicht auf NULL gesetzt oder den Speicher zugewiesen haben, auf den sie verweist?

Es gibt zwei Szenarien, in denen das Setzen des Zeigers auf NULL übersprungen werden kann:

  • die Zeigervariable verlässt sofort den Gültigkeitsbereich
  • sie haben die Semantik des Zeigers überladen und verwenden seinen Wert nicht nur als Speicherzeiger, sondern auch als Schlüssel oder Rohwert. Dieser Ansatz leidet jedoch unter anderen Problemen.

Zu argumentieren, dass das Setzen des Zeigers auf NULL Fehler für mich verbergen könnte, klingt nach dem Argument, dass Sie einen Fehler nicht beheben sollten, weil der Fix einen anderen Fehler verbergen könnte. Die einzigen Fehler, die möglicherweise angezeigt werden, wenn der Zeiger nicht auf NULL gesetzt ist, sind die, die versuchen, den Zeiger zu verwenden. Aber es auf NULL zu setzen, würde tatsächlich genau den gleichen Fehler verursachen, der auftreten würde, wenn Sie es mit freiem Speicher verwenden, nicht wahr?

2
Franci Penov

In einem gut strukturierten Programm mit entsprechender Fehlerprüfung gibt es keinen Grund nicht, es auf null zu setzen. 0 ist in diesem Zusammenhang ein allgemein anerkannter ungültiger Wert. Scheitern Sie hart und scheitern Sie bald.

Viele der Argumente gegen die Zuweisung von 0 schlagen vor, dass es könnte einen Fehler verbergen oder den Kontrollfluss erschweren. Grundsätzlich handelt es sich entweder um einen vorgelagerten Fehler (nicht um Ihre Schuld (Entschuldigung für das schlechte Wortspiel)) oder um einen anderen Fehler im Namen des Programmierers - vielleicht sogar um einen Hinweis darauf, dass der Programmablauf zu komplex geworden ist.

Wenn der Programmierer die Verwendung eines Zeigers einführen möchte, der als spezieller Wert Null sein kann, und alle erforderlichen Umgehungsinformationen schreiben möchte, ist dies eine Komplikation, die er absichtlich eingeführt hat. Je besser die Quarantäne ist, desto eher finden Sie Fälle von Missbrauch und desto weniger können sie auf andere Programme übertragen werden.

Gut strukturierte Programme können mithilfe von C++ - Funktionen entworfen werden, um diese Fälle zu vermeiden. Sie können Verweise verwenden oder einfach sagen, dass das Übergeben/Verwenden von Nullen oder ungültigen Argumenten ein Fehler ist - ein Ansatz, der auch für Container wie Smart Pointer gilt. Die Erhöhung des konsistenten und korrekten Verhaltens verhindert, dass diese Fehler weit kommen.

Von dort aus haben Sie nur einen sehr begrenzten Bereich und Kontext, in dem ein Nullzeiger vorhanden sein kann (oder zulässig ist).

Dasselbe kann auf Zeiger angewendet werden, die nicht const sind. Es ist trivial, dem Wert eines Zeigers zu folgen, da sein Bereich so klein ist und die unsachgemäße Verwendung überprüft und genau definiert wird. Wenn Ihr Toolset und Ihre Techniker dem Programm nach einem kurzen Lesevorgang nicht folgen können oder wenn eine unangemessene Fehlerprüfung oder ein inkonsistenter/lässiger Programmablauf vorliegt, haben Sie andere, größere Probleme.

Schließlich hat Ihr Compiler und Ihre Umgebung wahrscheinlich einige Sicherheitsvorkehrungen für die Zeiten, in denen Sie Fehler einführen (kritzeln), Zugriffe auf den freigegebenen Speicher erkennen und andere verwandte UB abfangen möchten. Sie können ähnliche Diagnosen auch in Ihre Programme integrieren, ohne dass dies Auswirkungen auf vorhandene Programme hat.

2
justin

"Es gibt Zeiten, in denen es gut ist, und Zeiten, in denen es sinnlos ist und Fehler verbergen kann."

Ich sehe zwei Probleme: Den einfachen Code:

delete myObj;
myobj = 0

wird zum For-Liner in Multithread-Umgebungen:

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

Die "Best Practice" von Don Neufeld gilt nicht immer. Z.B. In einem Automotive-Projekt mussten wir Zeiger auch in Destruktoren auf 0 setzen. Ich kann mir vorstellen, dass solche Regeln in sicherheitskritischer Software keine Seltenheit sind. Es ist einfacher (und weiser), ihnen zu folgen, als zu versuchen, das Team/den Code-Prüfer für jeden im Code verwendeten Zeiger davon zu überzeugen, dass eine Zeile, die diesen Zeiger auf Null setzt, überflüssig ist.

Eine andere Gefahr besteht darin, diese Technik in Code zu verwenden, der Ausnahmen verwendet:

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

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

In einem solchen Code führen Sie entweder zu einem Ressourcenleck und verschieben das Problem oder der Prozess stürzt ab.

Diese beiden Probleme, die mir spontan durch den Kopf gehen (Herb Sutter würde mit Sicherheit mehr sagen), werfen für mich alle Fragen der Art "Wie vermeide ich die Verwendung von intelligenten Zeigern und erledige die Arbeit sicher mit normalen Zeigern" als obsolet auf.

1

Lassen Sie mich erweitern, was Sie bereits in Ihre Frage gestellt haben.

Folgendes haben Sie in Form von Aufzählungszeichen in Ihre Frage eingefügt:


Das Setzen von Zeigern auf NULL nach dem Löschen ist in C++ keine allgemein anerkannte Vorgehensweise. Es gibt Zeiten, in denen:

  • das ist eine gute Sache
  • und Zeiten, in denen es sinnlos ist und Fehler verbergen kann.

Es gibt jedoch keine Zeiten , in denen dies schlecht ist ! Sie werden nicht weitere Fehler einführen, indem Sie sie explizit auf Null setzen. Sie werden nicht auslaufen Speicher, Sie werden nicht undefiniertes Verhalten verursachen.

Also, wenn Sie Zweifel haben, machen Sie es einfach null.

Wenn Sie jedoch das Gefühl haben, dass Sie einen Zeiger explizit auf Null setzen müssen, dann klingt dies so, als hätten Sie eine Methode nicht ausreichend aufgeteilt und sollten sich den Refactoring-Ansatz mit dem Namen "Methode extrahieren" ansehen, um die Methode aufzuteilen separate Teile.

Ja.

Der einzige "Schaden", den es anrichten kann, ist die Einführung von Ineffizienz (eine unnötige Speicheroperation) in Ihr Programm. In den meisten Fällen ist dieser Overhead im Verhältnis zu den Kosten für die Zuweisung und Freigabe des Speicherblocks jedoch unbedeutend.

Wenn Sie dies nicht tun, werden Sie eines Tages einige dereferenzierte Fehler haben.

Ich benutze immer ein Makro zum Löschen:

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

(und Ähnliches für ein Array, free (), Handles freigeben)

Sie können auch "Selbstlösch" -Methoden schreiben, die auf den Zeiger des aufrufenden Codes verweisen, sodass der Zeiger des aufrufenden Codes auf NULL gesetzt wird. So löschen Sie beispielsweise einen Teilbaum vieler Objekte:

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;
}

bearbeiten

Ja, diese Techniken verletzen einige Regeln bezüglich der Verwendung von Makros (und heutzutage könnten Sie wahrscheinlich mit Vorlagen dasselbe Ergebnis erzielen) - aber wenn ich über viele Jahre hinweg niemals auf toten Speicher zugreife - eins von den fiesesten und schwierigsten und zeitaufwändigsten Problemen, denen Sie begegnen können. In der Praxis haben sie über viele Jahre hinweg effektiv eine ganze Klasse von Fehlern aus jedem Team beseitigt, in dem ich sie vorgestellt habe.

Es gibt auch viele Möglichkeiten, wie Sie das Obige implementieren können. Ich möchte nur die Idee veranschaulichen, Menschen zum Nullen eines Zeigers zu zwingen, wenn sie ein Objekt löschen, anstatt ihnen ein Mittel bereitzustellen, den Speicher freizugeben, der den Zeiger des Aufrufers nicht auf Null setzt .

Das obige Beispiel ist natürlich nur ein Schritt in Richtung eines Auto-Zeigers. Was ich nicht vorgeschlagen habe, weil das OP speziell nach dem Fall gefragt hat, dass kein Auto-Pointer verwendet wird.

1
Jason Williams

Wenn der Code nicht zum leistungskritischsten Teil Ihrer Anwendung gehört, halten Sie ihn einfach und verwenden Sie einen shared_ptr:

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

Es führt eine Referenzzählung durch und ist threadsicher. Sie finden es im Namespace tr1 (std :: tr1, #include <memory>) oder, falls Ihr Compiler es nicht bereitstellt, bei boost.

0
beta4

Es gibt immer baumelnde Zeiger , um die man sich sorgen muss.

0
GrayWizardx

Wenn Sie den Zeiger vor der erneuten Verwendung neu zuordnen (dereferenzieren, an eine Funktion übergeben usw.), ist es nur eine zusätzliche Operation, den Zeiger auf NULL zu setzen. Wenn Sie jedoch nicht sicher sind, ob es vor der erneuten Verwendung neu zugewiesen wird, ist es eine gute Idee, es auf NULL zu setzen.

Wie viele gesagt haben, ist es natürlich viel einfacher, nur intelligente Zeiger zu verwenden.

Bearbeiten: Wie Thomas Matthews in dieser früheren Antwort sagte, ist es seitdem nicht erforderlich, NULL zuzuweisen, wenn ein Zeiger in einem Destruktor gelöscht wird es wird nicht mehr verwendet, da das Objekt bereits zerstört wird.

0
Dustin

Ich kann mir vorstellen, einen Zeiger nach dem Löschen auf NULL zu setzen, was in seltenen Fällen nützlich ist, wenn es ein legitimes Szenario gibt, in dem er in einer einzelnen Funktion (oder einem einzelnen Objekt) wiederverwendet wird. Andernfalls ergibt es keinen Sinn - ein Zeiger muss auf etwas Sinnvolles zeigen, solange es existiert - Punkt.

0