Frage

Ich werde zunächst sagen, Verwenden Sie intelligente Zeiger und Sie müssen sich darüber nie Sorgen machen.

Was sind die Probleme mit dem folgenden Code?

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

Dies wurde von ausgelöst von eine Antwort und Kommentare zu einer anderen Frage. Ein Kommentar von Neil Butterworth erzeugte ein paar Upvotes:

Das Einstellen von Zeiger auf Null nach dem Löschen ist keine universelle gute Praxis in C ++. Es gibt Zeiten, in denen es eine gute Sache 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 erleuchtet mich.

War es hilfreich?

Lösung

Wenn Sie einen Zeiger auf 0 festlegen (was in Standard C ++ "null" ist, ist die Nulldefinition von C etwas unterschiedlich) Stürze bei Doppel -Deletten.

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 keine gelöschten Zeiger auf 0 festlegen, werden Sie in Schwierigkeiten geraten, wenn Sie doppelte Löschungen durchführen. Ein Argument gegen das Einstellen von Zeigern auf 0 nach dem Löschen wäre, dass dies nur doppelt löschende Fehler maskiert und sie unberechtigt lässt.

Natürlich ist es am besten, keine doppelten Fehler zu haben, aber abhängig von der Eigentümersemantik und den Objektlebenszyklen kann dies in der Praxis schwer zu erreichen sein. Ich bevorzuge einen maskierten Doppel -Löschen -Fehler gegenüber UB.

Schließlich schlage ich vor, dass Sie sich ansehen std::unique_ptr für strenge/einzigartige Besitzer, std::shared_ptr Für gemeinsame Eigentümer oder eine andere Implementierung von Smart Zeiger, abhängig von Ihren Anforderungen.

Andere Tipps

Wenn Sie Zeiger auf Null setzen, nachdem Sie gelöscht haben, worauf es darauf hingewiesen hat, dass es sicherlich nicht schaden kann, ist es oft ein Pflaster über ein grundlegenderes Problem: Warum verwenden Sie überhaupt einen Zeiger? Ich kann zwei typische Gründe sehen:

  • Sie wollten einfach etwas, das auf dem Haufen zugewiesen wurde. In diesem Fall wäre es viel sicherer und sauberer gewesen, es in ein Raii -Objekt zu wickeln. Beenden Sie das Raii -Objektbereich, wenn Sie das Objekt nicht mehr benötigen. So geht das std::vector Arbeitet und es löst das Problem, versehentlich Zeiger für das umgegangene Gedächtnis zu lassen. Es gibt keine Zeiger.
  • Oder vielleicht wollten Sie eine komplexe gemeinsame Eigentümersemantik. Der Zeiger kehrte von zurück new könnte nicht dasselbe sein wie das, das delete wird aufgerufen. 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 hinterlassen, machen Sie es falsch. Der Zeiger sollte nicht da sein, um auf Müll zu verweisen. Warum übernimmt es keine Verantwortung für die Gewährleistung seiner Gültigkeit? Warum endet sein Zielfernrohr nicht, wenn das Point-to-Objekt dies tut?

Ich habe eine noch bessere Best Practice: Wenn möglich, beenden Sie den Bereich der Variablen!

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

Ich setze immer einen Zeiger NULL (jetzt nullptr) Nach dem Löschen der Objekte zeigt es auf.

  1. Es kann dazu beitragen, viele Hinweise auf den befreiten Speicher zu erfassen (vorausgesetzt, Ihre Plattformfehler auf einem Deref eines Nullzeigers).

  2. Es wird nicht alle Verweise auf Free'd Memory erfasst, wenn beispielsweise Kopien des Zeigers herumliegen. Aber einige sind besser als keine.

  3. Es wird ein Doppelverteidiger maskieren, aber ich finde, diese sind weitaus seltener als Zugriffe auf bereits befreiten Speicher.

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

  5. Wenn Sie bereits Raii verwenden, gibt es nicht viele deletes in Ihrem Code zunächst, also überzeugt mich das Argument, dass die zusätzliche Aufgabe zu Unordnung führt.

  6. Es ist oft bequem, beim Debuggen den Nullwert zu sehen und nicht einen abgestandenen Zeiger.

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

Ich setze auch andere Arten von Ressourcenhandles auf den Wert ohne Ressourcen, wenn die Ressource frei ist (was normalerweise nur im Destruktor eines Raii-Wrappers ist, der zur Einkapselung der Ressource geschrieben wurde).

Ich habe an einem großen (9 Millionen Aussagen) kommerziellen Produkt (hauptsächlich in C) gearbeitet. Irgendwann haben wir Makromagie verwendet, um den Zeiger herauszuholen, als die Erinnerung befreit wurde. Dies enthüllte sofort viele lauernde Fehler, die sofort behoben wurden. Soweit ich mich erinnern kann, hatten wir nie einen doppeltfreien Fehler.

Aktualisieren: Microsoft ist der Ansicht, dass es eine gute Praxis für die Sicherheit ist und empfiehlt die Praxis in ihren SDL -Richtlinien. Anscheinend wird MSVC ++ 11 Stampfen Sie den gelöschten Zeiger automatisch (unter vielen Umständen), wenn Sie mit der Option /SDL kompilieren.

Erstens gibt es beispielsweise viele vorhandene Fragen zu diesem und eng verwandten Themen Warum löscht das Löschen des Zeigers nicht auf Null?.

In Ihrem Code das Problem, was in (Verwendung P) verwendet wird. Zum Beispiel, wenn Sie einen solchen Code haben:

Foo * p2 = p;

Wenn Sie P auf Null setzen, wird es sehr wenig erreicht, da Sie immer noch den Zeiger P2 machen müssen, über den Sie sich Sorgen machen müssen.

Dies bedeutet nicht, dass das Einstellen eines Zeigers auf Null immer sinnlos ist. Wenn P beispielsweise eine Mitgliedsvariable wäre, die auf eine Ressource zeigte, deren Lebensdauer nicht genau derselbe war wie die Klasse P, dann könnte das Einstellen von P auf Null ein nützlicher Weg sein, um das Vorhandensein oder Fehlen der Ressource anzuzeigen.

Wenn es nach dem mehr Code gibt delete, Ja. Wenn der Zeiger in einem Konstruktor oder am Ende der Methode oder Funktion gelöscht wird, ist Nr.

Der Punkt dieses Gleichnisses besteht darin, den Programmierer während der Laufzeit daran zu erinnern, dass das Objekt bereits gelöscht wurde.

Eine noch bessere Praxis besteht darin, intelligente Zeiger (gemeinsam genutzt oder abgebildet) zu verwenden, die ihre Zielobjekte automatisch löschen.

Wie andere gesagt haben, delete ptr; ptr = 0; wird nicht dazu führen, dass Dämonen aus der Nase fliegen. Es fördert jedoch die Verwendung von ptr Als Flagge. Der Code wird übersät mit delete und setzen Sie den Zeiger auf NULL. Der nächste Schritt ist zu streuen if (arg == NULL) return; durch Ihren Code, um vor der versehentlichen Verwendung von a zu schützen NULL Zeiger. Das Problem tritt auf, sobald die Schecks dagegen NULL Werden Sie Ihr Hauptmittel zur Überprüfung des Zustands eines Objekts oder eines Programms.

Ich bin sicher, dass es einen Code riecht, einen Zeiger als Flagge irgendwo zu verwenden, aber ich habe keinen gefunden.

Ich werde Ihre Frage leicht ändern:

Würden Sie einen nicht initialisierten Zeiger verwenden? Weißt du, eine, die du nicht auf Null gesetzt hast oder den Speicher zuteilt, auf den er verweist?

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

  • Die Zeigervariable verläuft sofort aus dem Zielfernrohr
  • Sie haben die Semantik des Zeigers überlastet und verwenden seinen Wert nicht nur als Speicherzeiger, sondern auch als Schlüssel- oder Rohwert. Dieser Ansatz leidet jedoch unter anderen Problemen.

Wenn man argumentiert, dass das Festlegen des Zeigers auf Null Fehler für mich verbergen könnte, klingt man so aus, als ob Sie keinen Fehler beheben sollten, da die Fix einen anderen Fehler ausblenden könnte. Die einzigen Fehler, die zeigen könnten, ob der Zeiger nicht auf Null gesetzt ist, wären diejenigen, die versuchen, den Zeiger zu verwenden. Aber das Einstellen auf Null würde tatsächlich genau den gleichen Fehler wie das zeigen, wenn Sie es mit befreitem Speicher verwenden würden, nicht wahr?

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

Für mich ist die Frage nicht "Ist das eine gute Idee?" Aber "Welches Verhalten würde ich verhindern oder zulassen, dass es erfolgreich ist?" Wenn dies beispielsweise anderen Code ermöglicht, zu sehen, dass der Zeiger nicht mehr verfügbar ist, warum versucht ein anderer Code überhaupt, befreite Zeiger nach ihrer Befreiung zu betrachten? Normalerweise ist es ein Fehler.

Es leistet auch mehr Arbeit als nötig und behindert das Debuggen nach dem Mortem. Je weniger Sie den Speicher berühren, nachdem Sie es nicht benötigen, desto einfacher ist es herauszufinden, warum etwas abgestürzt ist. Oft habe ich mich darauf verlassen, dass der Speicher in einem ähnlichen Zustand ist, als ein bestimmter Fehler den Fehler diagnostiziert und behebt.

Explizit Nulling nach dem Löschen schlägt einem Leser vor, dass der Zeiger etwas repräsentiert, das konzeptionell ist Optional. Wenn ich sehen würde, dass das gemacht wurde, würde ich mir Sorgen machen, dass der Zeiger überall in der Quelle verwendet wird, dass er zuerst gegen Null getestet werden sollte.

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

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, werde ich mit denen, die sagen, das Beste zu sagen, dass es das Beste ist, ihn aus dem Spielraum zu gehen. Anschließend verwenden Sie den Compiler, um die Möglichkeit von schlechten Derferen zur Laufzeit zu verhindern.

Das ist das Baby in all dem C ++ - Badewasser sollte es nicht rauswerfen. :)

In einem gut strukturierten Programm mit entsprechender Fehlerprüfung gibt es keinen Grund nicht um es null zuweisen. 0 steht allein als allgemein anerkannter ungültiger Wert in diesem Zusammenhang. Scheitern Sie hart und scheitern Sie bald.

Viele der Argumente gegen die Zuweisung 0 Schlagen Sie das vor könnte einen Fehler ausblenden oder den Steuerfluss komplizieren. Grundsätzlich ist dies entweder ein vorgelagerter Fehler (nicht Ihre Schuld (Entschuldigung für das schlechte Wortspiel) oder einen anderen Fehler im Namen des Programmierers - vielleicht sogar ein Hinweis darauf, dass der Programmfluss zu komplex geworden ist.

Wenn der Programmierer die Verwendung eines Zeigers einführen möchte, der als besonderer Wert null ist, und alle notwendigen Ausweichen zu schreiben, 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 sich in andere Programme ausbreiten.

Gut strukturierte Programme können mit C ++ - Funktionen ausgelegt werden, um diese Fälle zu vermeiden. Sie können Referenzen verwenden, oder Sie können einfach sagen, dass "Null- oder Ungültige Argumente mithilfe von Argumenten bestehen/verwenden, ist ein Fehler" - ein Ansatz, der für Container wie intelligente Zeiger gleichermaßen anwendbar ist. Das Erhöhen des konsequenten und korrekten Verhaltens verbietet diese Fehler, weit zu kommen.

Von dort aus haben Sie nur einen sehr begrenzten Umfang und einen sehr begrenzten Kontext, in dem ein Nullzeiger existieren kann (oder erlaubt ist).

Das Gleiche kann auf Zeiger angewendet werden, die nicht sind const. Der Wert eines Zeigers zu folgen ist trivial, da sein Umfang so gering ist und die unsachgemäße Verwendung überprüft und gut definiert ist. Wenn Ihr Toolset und Ihre Ingenieure das Programm nach einer schnellen Lektüre nicht befolgen oder unangemessene Fehlerprüfung oder inkonsistentes/mildernes Programmfluss vorliegen, haben Sie andere, größere Probleme.

Schließlich verfügt Ihr Compiler und Ihre Umgebung wahrscheinlich für die Zeiten, in denen Sie Fehler einführen möchten (kritzeln), Zugriffe auf den befreiten Speicher zu erkennen und andere verwandte UB zu fangen. Sie können auch ähnliche Diagnostik in Ihre Programme einführen, häufig ohne die vorhandenen Programme zu beeinflussen.

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

Hier ist, was Sie in Ihre Frage in Kugelpunktform gestellt haben:


Das Einstellen von Zeiger auf Null nach dem Löschen ist keine universelle gute Praxis in C ++. Es gibt Zeiten, in denen:

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

Es gibt jedoch keine Zeiten Wenn dies der Fall ist Schlecht! Du wirst nicht Führen Sie mehr Fehler ein, indem Sie es explizit nullieren, Sie werden es nicht tun Leck Speicher, du wirst nicht undefinedes Verhalten verursachen passieren.

Also, im Zweifeln, null es einfach.

Wenn Sie jedoch das Gefühl haben, dass Sie einen Zeiger explizit null müssen, klingt dies für mich so, als hätten Sie eine Methode nicht genug aufgeteilt und sollten sich den Refactoring -Ansatz ansehen, der als "Methode extrahieren" bezeichnet wird, um die Methode in die Methode aufzuteilen separate Teile.

Ja.

Der einzige "Schaden", den es tun kann, ist die Einführung von Ineffizienz (einen unnötigen Speicherbetrieb) in Ihr Programm - aber dieser Overhead ist in den meisten Fällen in Bezug auf die Kosten für die Zuweisung und Befreiung des Speicherblocks unbedeutend.

Wenn Sie es nicht tun, Sie, Sie Wille Haben Sie eines Tages einige böse Zeiger Derferce -Fehler.

Ich verwende immer ein Makro zum Löschen:

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

(Und ähnlich für ein Array, Free (), Griffe veröffentlichen)

Sie können auch "Self Delete" -Methoden schreiben, die einen Verweis auf den Zeiger des Anrufcodes verweisen, damit sie den Zeiger des Anrufcodes auf Null zwingen. Zum Beispiel zum Löschen eines Teilbaums 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 verstoßen gegen einige Regeln für die Verwendung von Makros (und ja, heutzutage könnten Sie wahrscheinlich das gleiche Ergebnis mit Vorlagen erzielen) - aber durch die Verwendung von über vielen Jahren i niemals Zugriff auf Dead Memory - eines der schlimmsten und schwierigsten und zeitaufwändigsten, mit denen Sie konfrontiert werden können, denen Sie konfrontiert sind. In der Praxis über viele Jahre haben sie eine WHJOLE -Klasse von Fehlern von jedem Team, in das ich sie eingeführt hat, effektiv beseitigt.

Es gibt auch viele Möglichkeiten, wie Sie die oben genannten implementieren können. Ich versuche nur, die Idee zu veranschaulichen, Menschen zu zwingen, einen Zeiger zu nutzen, wenn sie ein Objekt löschen .

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 fragte, ob ich keinen Autozeiger benutzte.

"Es gibt Zeiten, in denen es eine gute Sache ist, und Zeiten, in denen es sinnlos ist und Fehler verbergen kann"

Ich kann zwei Probleme sehen: diesen einfachen Code:

delete myObj;
myobj = 0

wird zu einem Verkehr in der multitHhread-Umgebung:

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

Die "Best Practice" von Don Neufeld gilt nicht immer. ZB in einem Automobilprojekt mussten wir auch bei Zerstörern Zeiger auf 0 setzen. Ich kann mir vorstellen, dass in der Sicherheits-kritischen Software solche Regeln nicht ungewöhnlich sind. Es ist einfacher (und weise), ihnen zu folgen, als zu versuchen, das Team/Code-Checker für jeden Zeiger in Code zu überzeugen, dass eine Zeile, die diesen Zeiger null ist, überflüssig ist.

Eine weitere Gefahr besteht darin, diese Technik bei Ausnahmen zu verwenden, um Code zu verwenden:

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

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

In einem solchen Code produzieren Sie entweder Ressourcenlaud und verschieben das Problem oder der Prozess stürzt ab.

Diese beiden Probleme gehen also spontan durch meinen Kopf (Herb Sutter würde sicher mehr sagen) für mich alle Fragen der Art "Wie man die Verwendung von Smart-Pointern vermeidet und die Arbeit mit normalen Zeigern sicher erledigt" als veraltet.

Es gibt immer Zeiger baumeln sich Sorgen machen um.

Wenn Sie den Zeiger neu zuweisen, bevor Sie ihn erneut verwenden (Derference, ihn an eine Funktion usw. übergeben), ist es nur eine zusätzliche Operation, den Zeiger Null zu machen. Wenn Sie sich jedoch nicht sicher sind, ob es neu zugewiesen wird oder nicht, bevor es erneut verwendet 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 sagte Diese frühere Antwort, Wenn ein Zeiger in einem Destruktor gelöscht wird, muss ihm keine Null zuweisen, da er nicht erneut verwendet wird, da das Objekt bereits zerstört wird.

Ich kann mir vorstellen, einen Zeiger auf Null zu setzen, nachdem er ihn gelöscht hat legitim Szenario der Wiederverwendung in einer einzigen Funktion (oder in Objekt). Ansonsten macht es keinen Sinn - ein Zeiger muss auf etwas Sinnvolles hinweisen, solange es existiert - Zeitraum.

Wenn der Code nicht zum leistungsstärksten Teil Ihrer Anwendung gehört, halten Sie ihn einfach und verwenden Sie ein Shared_Ptr:

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

Es führt Referenzzählung durch und ist thread-sicher. Sie finden es in der TR1 (std :: tr1 namespace, #include <Meenion>) oder wenn Ihr Compiler es nicht zur Verfügung stellt, erhalten Sie es vom Boost.

Lizenziert unter: CC-BY-SA mit Zuschreibung
Nicht verbunden mit StackOverflow
scroll top