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


149

Ich beginne mit den Worten: Verwenden Sie intelligente Zeiger, und Sie müssen sich darüber keine Sorgen machen.

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 zu einer anderen Frage ausgelöst . Ein Kommentar von Neil Butterworth sorgte für einige positive Stimmen:

Das Setzen von Zeigern auf NULL nach dem Löschen ist in C ++ keine universelle bewährte Methode. 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 hat mich aufgeklärt.


6
@Andre: Technisch ist es undefiniert. Es ist wahrscheinlich, dass Sie auf denselben Speicher wie zuvor zugreifen, dieser kann jedoch jetzt von etwas anderem verwendet werden. Wenn Sie den Speicher zweimal löschen, kann dies die Programmausführung auf schwer zu findende Weise beeinträchtigen. Es ist jedoch sicher für deleteeinen Nullzeiger, was ein Grund dafür ist, dass das Nullstellen eines Zeigers gut sein kann.
David Thornley

5
@ André Pena, es ist undefiniert. Oft ist es nicht einmal wiederholbar. Sie setzen den Zeiger auf NULL, um den Fehler beim Debuggen besser sichtbar zu machen und ihn möglicherweise wiederholbarer zu machen.
Mark Ransom

3
@ André: Niemand weiß es. Es ist undefiniertes Verhalten. Es kann mit einer Zugriffsverletzung abstürzen oder den vom Rest der Anwendung verwendeten Speicher überschreiben. Der Sprachstandard übernimmt keine Garantie dafür, was passiert. Daher können Sie Ihrer Anwendung nicht vertrauen, wenn sie einmal passiert ist. Es hätte die Atomraketen abfeuern oder Ihre Festplatte formatieren können. Es kann das Gedächtnis Ihrer App beschädigen oder Dämonen aus Ihrer Nase fliegen lassen. Alle Wetten sind geschlossen.
Jalf

16
Die fliegenden Dämonen sind ein Feature, kein Bug.
Jball

3
Diese Frage ist kein Duplikat, da es sich bei der anderen Frage um C und bei dieser um C ++ handelt. Viele der Antworten hängen von Dingen wie intelligenten Zeigern ab, die in C ++ nicht verfügbar sind.
Adrian McCarthy

Antworten:


84

Durch Setzen eines Zeigers auf 0 (was in Standard-C ++ "null" ist, die NULL-Definition von C ist etwas anders) werden Abstürze bei doppelten Löschvorgängen vermieden.

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 doppelt löschen. Ein Argument gegen das Setzen von Zeigern auf 0 nach dem Löschen wäre, dass dadurch nur doppelte Löschfehler maskiert und unbehandelt bleiben.

Es ist natürlich am besten, keine doppelten Löschfehler zu haben, aber abhängig von der Besitzersemantik und den Objektlebenszyklen kann dies in der Praxis schwierig zu erreichen sein. Ich bevorzuge einen maskierten doppelten Löschfehler gegenüber UB.

Als Nebenbemerkung zum Verwalten der Objektzuweisung schlage ich vor, dass Sie sich je nach Ihren Anforderungen einen Blick auf std::unique_ptrstrikte / singuläre Eigentumsverhältnisse, std::shared_ptrauf gemeinsame Eigentumsverhältnisse oder eine andere Implementierung eines intelligenten Zeigers werfen .


13
Ihre Anwendung stürzt nicht immer bei einem doppelten Löschvorgang ab. Je nachdem, was zwischen den beiden Löschvorgängen passiert, kann alles passieren. Höchstwahrscheinlich beschädigen Sie Ihren Heap und stürzen später in einem völlig unabhängigen Code ab. Während ein Segfault normalerweise besser ist, als den Fehler stillschweigend zu ignorieren, ist der Segfault in diesem Fall nicht garantiert und von fragwürdigem Nutzen.
Adam Rosenfield

28
Das Problem hierbei ist die Tatsache, dass Sie eine doppelte Löschung haben. Wenn Sie den Zeiger auf NULL setzen, wird nur die Tatsache ausgeblendet, dass er nicht korrigiert oder sicherer wird. Stellen Sie sich einen Mainainer vor, der ein Jahr später zurückkommt und foo gelöscht sieht. Er glaubt jetzt, dass er den Zeiger wiederverwenden kann, leider kann er das zweite Löschen verpassen (es kann nicht einmal in derselben Funktion sein) und jetzt wird die Wiederverwendung des Zeigers jetzt durch das zweite Löschen verworfen. Jeder Zugriff nach dem zweiten Löschen ist jetzt ein großes Problem.
Martin York

11
Es ist wahr, dass das Setzen des Zeigers NULLeinen doppelten Löschfehler maskieren kann. (Einige mögen diese Maske tatsächlich als Lösung betrachten - sie ist es, aber nicht sehr gut, da sie dem Problem nicht auf den Grund geht.) Wenn Sie sie jedoch nicht (weit!) Mehr auf NULL-Masken setzen Häufige Probleme beim Zugriff auf die Daten nach dem Löschen.
Adrian McCarthy

AFAIK, std :: auto_ptr wurde im kommenden c ++ Standard
rafak

Ich würde nicht sagen, veraltet, es klingt so, als wäre die Idee einfach weg. Vielmehr wird es durch unique_ptrdie auto_ptrBewegungssemantik ersetzt , die das tut, was versucht wurde.
GManNickG

55

Das Setzen von Zeigern auf NULL, nachdem Sie das gelöscht haben, worauf es hingewiesen hat, kann sicherlich nicht schaden, aber es ist oft eine Art Pflaster für ein grundlegenderes Problem: Warum verwenden Sie überhaupt einen Zeiger? Ich kann zwei typische Gründe sehen:

  • Sie wollten einfach etwas auf dem Haufen zugewiesen. In diesem Fall wäre das Einwickeln in ein RAII-Objekt viel sicherer und sauberer gewesen. Beenden Sie den Bereich des RAII-Objekts, wenn Sie das Objekt nicht mehr benötigen. So std::vectorfunktioniert es und es löst das Problem, versehentlich Zeiger auf freigegebenen Speicher zu belassen. Es gibt keine Zeiger.
  • Oder vielleicht wollten Sie eine komplexe Semantik für gemeinsames Eigentum. Der von zurückgegebene Zeiger ist newmöglicherweise nicht derselbe wie der deleteaufgerufene. In der Zwischenzeit haben möglicherweise mehrere Objekte das Objekt gleichzeitig verwendet. In diesem Fall wäre ein gemeinsamer Zeiger oder ähnliches vorzuziehen gewesen.

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


15
Sie argumentieren also, dass es überhaupt keinen rohen Zeiger geben sollte und dass alles, was diesen Zeiger betrifft, nicht mit dem Begriff "gute Praxis" gesegnet werden sollte? Meinetwegen.
Mark Ransom

7
Na ja, mehr oder weniger. Ich würde nicht sagen, dass nichts , was einen rohen Zeiger beinhaltet, als gute Praxis bezeichnet werden kann. Nur dass es eher die Ausnahme als die Regel ist. Normalerweise ist das Vorhandensein des Zeigers ein Indikator dafür, dass auf einer tieferen Ebene etwas nicht stimmt.
Jalf

3
Aber um die unmittelbare Frage zu beantworten, nein, ich sehe nicht, wie das Setzen von Zeigern auf Null jemals Fehler verursachen kann.
Jalf

7
Ich bin anderer Meinung - es gibt Fälle, in denen ein Zeiger gut zu verwenden ist. Zum Beispiel gibt es 2 Variablen auf dem Stapel und Sie möchten eine davon auswählen. Oder Sie möchten eine optionale Variable an eine Funktion übergeben. Ich würde sagen, Sie sollten niemals einen rohen Zeiger in Verbindung mit verwenden new.
Rlbond

4
Wenn ein Zeiger den Gültigkeitsbereich verlassen hat, sehe ich nicht, wie irgendetwas oder irgendjemand damit umgehen muss.
Jalf

43

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

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

17
Ja, RAII ist dein Freund. Wickeln Sie es in eine Klasse und es wird noch einfacher. Oder behandeln Sie den Speicher überhaupt nicht mit der STL!
Brian

24
Ja, das ist die beste Option. Beantwortet die Frage jedoch nicht.
Mark Ransom

3
Dies scheint nur ein Nebenprodukt der Verwendung des Zeitraums für Funktionsbereiche zu sein und geht das Problem nicht wirklich an. Wenn Sie Zeiger verwenden, übergeben Sie normalerweise Kopien davon mehrere Ebenen tief, und dann ist Ihre Methode wirklich bedeutungslos, um das Problem zu beheben. Ich bin damit einverstanden, dass gutes Design Ihnen hilft, die Fehler zu isolieren, aber ich denke nicht, dass Ihre Methode das primäre Mittel zu diesem Zweck ist.
San Jacinto

2
Wenn Sie dies tun könnten, warum würden Sie dann nicht einfach den Haufen vergessen und Ihren gesamten Speicher vom Stapel ziehen?
San Jacinto

4
Mein Beispiel ist absichtlich minimal. Beispielsweise wird das Objekt anstelle von neu möglicherweise von einer Factory erstellt. In diesem Fall kann es nicht auf den Stapel verschoben werden. Oder es wird nicht am Anfang des Bereichs erstellt, sondern befindet sich in einer Struktur. Was ich illustriere, ist, dass dieser Ansatz einen Missbrauch des Zeigers zur Kompilierungszeit findet , während NULLing zur Laufzeit einen Missbrauch findet .
Don Neufeld

31

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

  1. Es kann dabei helfen, viele Verweise auf freigegebenen Speicher abzufangen (vorausgesetzt, Ihre Plattform weist Fehler bei einem Deref eines Nullzeigers auf).

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

  3. Es wird ein doppeltes Löschen maskieren, 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 also nicht.

  5. Wenn Sie bereits RAII verwenden, enthält deleteIhr Code zunächst nicht viele s, sodass mich das Argument, dass die zusätzliche Zuweisung Unordnung verursacht, 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 ohne Ressource gesetzt, wenn die Ressource frei ist (normalerweise nur im Destruktor eines RAII-Wrappers, der zur Kapselung der Ressource geschrieben wurde).

Ich habe an einem großen (9 Millionen Aussagen) kommerziellen Produkt gearbeitet (hauptsächlich in C). An einem Punkt haben wir Makromagie verwendet, um den Zeiger auf Null zu setzen, wenn Speicher freigegeben wurde. Dies enthüllte sofort viele lauernde Fehler, die sofort behoben wurden. Soweit ich mich erinnern kann, hatten wir nie einen doppelten Fehler.

Update: Microsoft ist der Ansicht, dass dies eine bewährte Methode für die Sicherheit ist, und empfiehlt diese Vorgehensweise in den SDL-Richtlinien. Anscheinend stampft MSVC ++ 11 den gelöschten Zeiger automatisch (unter vielen Umständen), wenn Sie mit der Option / SDL kompilieren.


12

Erstens gibt es viele Fragen zu diesem und eng verwandten Themen, z. B. Warum nicht löschen, setzen Sie den Zeiger auf NULL? .

In Ihrem Code das Problem, was in (verwenden Sie p) vor sich geht. Zum Beispiel, wenn Sie 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, über 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 ist, die auf eine Ressource verweist, deren Lebensdauer nicht genau mit der Klasse übereinstimmt, die p enthält, kann das Setzen von p auf NULL eine nützliche Methode sein, um das Vorhandensein oder Fehlen der Ressource anzuzeigen.


1
Ich stimme zu, dass es Zeiten gibt, in denen es nicht hilft, aber Sie schienen zu implizieren, dass es aktiv schädlich sein könnte. War das deine Absicht oder habe ich es falsch gelesen?
Mark Ransom

1
Ob es eine Kopie des Zeigers gibt, ist für die Frage, ob die Zeigervariable auf NULL gesetzt werden soll, irrelevant. Das Setzen auf NULL ist aus dem gleichen Grund eine gute Praxis. Das Reinigen des Geschirrs nach dem Abendessen ist eine gute Praxis. Es ist zwar kein Schutz gegen alle Fehler, die ein Code haben kann, fördert jedoch die Gesundheit des Codes.
Franci Penov

2
@Franci Viele Leute scheinen dir nicht zuzustimmen. Und ob es eine Kopie gibt, ist sicherlich relevant, wenn Sie versuchen, die Kopie zu verwenden, nachdem Sie das Original gelöscht haben.

3
Franci, da ist ein Unterschied. Sie putzen Geschirr, weil Sie es wieder verwenden. Sie benötigen den Zeiger nicht, nachdem Sie ihn gelöscht haben. Es sollte das Letzte sein, was Sie tun. Besser ist es, die Situation insgesamt zu vermeiden.
GManNickG

1
Sie können eine Variable wiederverwenden, aber dann handelt es sich nicht mehr um defensive Programmierung. So haben Sie die Lösung für das vorliegende Problem entworfen. Das OP diskutiert, ob dieser Verteidigungsstil etwas ist, nach dem wir streben sollten, und nicht, ob wir jemals einen Zeiger auf Null setzen werden. Und im Idealfall auf Ihre Frage ja! Verwenden Sie keine Zeiger, nachdem Sie sie gelöscht haben!
GManNickG

7

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

In dieser Parabel soll der Programmierer zur Laufzeit daran erinnert werden, dass das Objekt bereits gelöscht wurde.

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


Alle (einschließlich des ursprünglichen Fragestellers) sind sich einig, dass intelligente Zeiger der richtige Weg sind. Code entwickelt sich. Nach dem Löschen ist möglicherweise nicht mehr Code vorhanden, wenn Sie ihn zum ersten Mal korrigieren. Dies wird sich jedoch wahrscheinlich im Laufe der Zeit ändern. Das Einfügen der Aufgabe hilft in diesem Fall (und kostet in der Zwischenzeit fast nichts).
Adrian McCarthy

3

Wie andere gesagt haben, delete ptr; ptr = 0;wird es nicht dazu führen, dass Dämonen aus deiner Nase fliegen. Es fördert jedoch die Verwendung ptrals eine Art Flagge. Der Code wird übersät deleteund setzt den Zeiger auf NULL. Der nächste Schritt besteht darin, if (arg == NULL) return;Ihren Code zu verteilen, um sich vor der versehentlichen Verwendung eines NULLZeigers zu schützen . Das Problem tritt auf, sobald die Überprüfungen gegen Sie NULLzu Ihrem primären Mittel zur Überprüfung des Status eines Objekts oder Programms werden.

Ich bin mir sicher, dass es einen Codegeruch gibt, irgendwo einen Zeiger als Flag zu verwenden, aber ich habe keinen gefunden.


9
Es ist nichts Falsches daran, einen Zeiger als Flag zu verwenden. Wenn Sie einen Zeiger verwenden und NULLkein gültiger Wert ist, sollten Sie stattdessen wahrscheinlich eine Referenz verwenden.
Adrian McCarthy

2

Ich werde Ihre Frage leicht ändern:

Würden Sie einen nicht initialisierten Zeiger verwenden? Weißt du, eine, die du nicht auf NULL gesetzt oder den Speicher zugewiesen hast, auf den sie zeigt?

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.

In der Zwischenzeit klingt das Argumentieren, dass das Setzen des Zeigers auf NULL Fehler für mich verbergen könnte, wie das Argumentieren, dass Sie einen Fehler nicht beheben sollten, da durch den Fix möglicherweise ein anderer Fehler ausgeblendet wird. Die einzigen Fehler, die auftreten können, wenn der Zeiger nicht auf NULL gesetzt ist, sind diejenigen, die versuchen, den Zeiger zu verwenden. Aber das Setzen auf NULL würde tatsächlich genau den gleichen Fehler verursachen, der sich zeigen würde, wenn Sie es mit freigegebenem Speicher verwenden, nicht wahr?


(A) "klingt so, als würde man argumentieren, dass man einen Fehler nicht beheben sollte" Das Nicht-Setzen eines Zeigers auf NULL ist kein Fehler. (B) "Aber das Setzen auf NULL würde tatsächlich genau den gleichen Fehler verursachen" Nein. Das Setzen auf NULL verbirgt das doppelte Löschen . (C) Zusammenfassung: Wenn Sie auf NULL setzen, wird das doppelte Löschen ausgeblendet, veraltete Referenzen werden jedoch angezeigt. Wenn Sie nicht auf NULL setzen, werden veraltete Verweise ausgeblendet, es werden jedoch doppelte Löschvorgänge angezeigt. Beide Seiten sind sich einig, dass das eigentliche Problem darin besteht, veraltete Verweise und doppelte Löschvorgänge zu korrigieren.
Mooing Duck

2

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

Für mich lautet die Frage nicht "Ist das eine gute Idee?" aber "Welches Verhalten würde ich verhindern oder zulassen, um dies zu erreichen?" Wenn auf diese Weise beispielsweise anderer Code erkennen kann, dass der Zeiger nicht mehr verfügbar ist, warum versucht dann anderer Code überhaupt, freigegebene Zeiger zu betrachten, nachdem sie freigegeben wurden? Normalerweise ist es ein Fehler.

Es erledigt auch mehr Arbeit als nötig und behindert das Post-Mortem-Debugging. Je weniger Sie den Speicher berühren, nachdem Sie ihn nicht benötigt haben, desto einfacher ist es herauszufinden, warum etwas abgestürzt ist. Ich habe mich oft 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

Das explizite Nullstellen nach dem Löschen legt dem Leser stark nahe, dass der Zeiger etwas darstellt, das konzeptionell optional ist . Wenn ich das sehen würde, 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, dass es das Beste ist, ihn aus dem Rahmen zu werfen. Dann verwenden Sie den Compiler, um die Möglichkeit fehlerhafter Dereferenzen zur Laufzeit zu verhindern.

Das ist das Baby im ganzen C ++ Badewasser, sollte es nicht rauswerfen. :) :)


2

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

Viele der Argumente gegen die Zuweisung 0legen nahe , dass es könnte einen Fehler oder komplizieren Steuerfluss verstecken. Grundsätzlich ist dies entweder ein Upstream-Fehler (nicht Ihre Schuld (Entschuldigung für das schlechte Wortspiel)) oder ein anderer 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 spezieller Wert null sein kann, und alle erforderlichen Ausweichmanöver darauf 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 sich auf andere Programme ausbreiten.

Gut strukturierte Programme können unter Verwendung von C ++ - Funktionen entworfen werden, um diese Fälle zu vermeiden. Sie können Referenzen verwenden oder einfach sagen, dass das Übergeben / Verwenden von null oder ungültigen Argumenten ein Fehler ist - ein Ansatz, der auch für Container wie Smart Pointer gilt. Das Erhöhen 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 möglicherweise ein Nullzeiger vorhanden ist (oder zulässig ist).

Gleiches kann auf Zeiger angewendet werden, die dies nicht sind const. Das Verfolgen des Werts eines Zeigers ist trivial, da sein Umfang so klein ist und die missbräuchliche Verwendung überprüft und genau definiert wird. Wenn Ihr Toolset und Ihre Techniker dem Programm nach einem schnellen Lesen nicht folgen können oder eine unangemessene Fehlerprüfung oder ein inkonsistenter / milder Programmfluss vorliegt, haben Sie andere, größere Probleme.

Schließlich verfügt Ihr Compiler und Ihre Umgebung wahrscheinlich über einige Schutzfunktionen für die Zeiten, in denen Sie Fehler (Scribbling) einführen, Zugriffe auf freigegebenen Speicher erkennen und andere verwandte UB abfangen möchten. Sie können ähnliche Programme auch in Ihre Programme einführen, häufig ohne Auswirkungen auf vorhandene Programme.


1

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

Folgendes haben Sie in Aufzählungszeichenform in Ihre Frage eingefügt:


Das Setzen von Zeigern auf NULL nach dem Löschen ist in C ++ keine universelle bewährte Methode. 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, in denen dies schlecht ist ! Sie werden keine weiteren Fehler einführen, indem Sie sie explizit auf Null setzen. Sie werden keinen Speicher verlieren . Sie werden kein undefiniertes Verhalten verursachen .

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

Wenn Sie jedoch der Meinung sind, dass Sie einen Zeiger explizit auf Null setzen müssen, klingt dies für mich so, als hätten Sie eine Methode nicht ausreichend aufgeteilt und sollten sich den Refactoring-Ansatz mit dem Namen "Extract method" ansehen, in den die Methode aufgeteilt werden soll separate Teile.


Ich bin nicht einverstanden mit "Es gibt keine Zeiten, in denen dies schlecht ist." Betrachten Sie die Menge an Cutter, die diese Redewendung einführt. In jeder Einheit, die etwas löscht, ist eine Kopfzeile enthalten, und alle diese Löschpositionen werden nur geringfügig unkomplizierter.
GManNickG

Es gibt Zeiten, in denen es schlecht ist. Wenn jemand versucht, Ihren Zeiger "Gelöscht jetzt Null" zu dereferenzieren, wenn er dies nicht sollte, stürzt er wahrscheinlich nicht ab und dieser Fehler ist "versteckt". Wenn sie Ihren gelöschten Zeiger dereferenzieren, der noch einen zufälligen Wert enthält, werden Sie dies wahrscheinlich bemerken und der Fehler wird leichter zu erkennen sein.
Carson Myers

@Carson: Meine Erfahrung ist genau das Gegenteil: Das Dereferenzieren eines Nullptr führt fast auf alle Arten zum Absturz der Anwendung und kann von einem Debugger abgefangen werden. Das Dereferenzieren eines baumelnden Zeigers führt normalerweise nicht sofort zu einem Problem, sondern führt häufig nur zu falschen Ergebnissen oder anderen Fehlern auf der ganzen Linie.
MikeMB

@ MikeMB Ich stimme vollkommen zu, meine Ansichten darüber haben sich in den letzten ~ 6,5 Jahren erheblich geändert
Carson Myers

Als Programmierer waren wir alle vor 6-7 Jahren jemand anderes :) Ich bin mir nicht mal sicher, ob ich es heute gewagt hätte, eine C / C ++ - Frage zu beantworten :)
Lasse V. Karlsen

1

Ja.

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

Wenn Sie es nicht tun, Sie werden einige böse Zeiger derefernce Fehler eines Tages.

Ich benutze immer ein Makro zum Löschen:

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

(und ähnliches für ein Array, free (), Freigabe von Handles)

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 verstoßen gegen einige Regeln zur Verwendung von Makros (und ja, heutzutage könnten Sie wahrscheinlich das gleiche Ergebnis mit Vorlagen erzielen) - aber durch die Verwendung über viele Jahre habe ich nie auf toten Speicher zugegriffen - eine der schlimmsten und schwierigsten und Am zeitaufwändigsten ist das Debuggen von Problemen, mit denen Sie konfrontiert werden können. In der Praxis haben sie über viele Jahre hinweg eine ganze Klasse von Fehlern aus jedem Team, in dem ich sie vorgestellt habe, effektiv beseitigt.

Es gibt auch viele Möglichkeiten, wie Sie das oben Genannte implementieren können. Ich versuche nur, die Idee zu veranschaulichen, Menschen zu zwingen, einen Zeiger auf NULL zu setzen, wenn sie ein Objekt löschen, anstatt ihnen die Möglichkeit zu geben, 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-Zeiger verwendet wird.


2
Makros sind eine schlechte Idee, insbesondere wenn sie wie normale Funktionen aussehen. Wenn Sie dies tun möchten, verwenden Sie eine Vorlagenfunktion.

2
Wow ... Ich habe noch nie so etwas gesehen, wie anObject->Delete(anObject)den anObjectZeiger ungültig zu machen . Das ist nur erschreckend. Sie sollten hierfür eine statische Methode erstellen, damit Sie TreeItem::Delete(anObject)zumindest dazu gezwungen sind .
D. Shawley

Tut mir leid, ich habe es als Funktion eingegeben, anstatt das richtige Großbuchstaben "Dies ist ein Makro" zu verwenden. Korrigiert. Fügte auch einen Kommentar hinzu, um mich besser zu erklären.
Jason Williams

Und du hast recht, mein schnell verprügeltes Beispiel war Müll! Fest :-). Ich habe nur versucht, ein kurzes Beispiel zu finden, um diese Idee zu veranschaulichen: Jeder Code, der einen Zeiger löscht, sollte sicherstellen, dass der Zeiger auf NULL gesetzt ist, selbst wenn jemand anderes (der Aufrufer) diesen Zeiger besitzt. Übergeben Sie daher immer einen Verweis auf den Zeiger, damit er zum Zeitpunkt des Löschens auf NULL gesetzt werden kann.
Jason Williams

1

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

Ich kann zwei Probleme sehen: Dieser einfache Code:

delete myObj;
myobj = 0

wird zu einem For-Liner in einer Multithread-Umgebung:

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

Die "Best Practice" von Don Neufeld gilt nicht immer. Zum Beispiel mussten wir in einem Automobilprojekt auch in Destruktoren Zeiger auf 0 setzen. Ich kann mir vorstellen, dass solche Regeln in sicherheitskritischer Software keine Seltenheit sind. Es ist einfacher (und klüger), ihnen zu folgen, als zu versuchen, das Team / die Codeprüfung für jeden im Code verwendeten Zeiger davon zu überzeugen, dass eine Zeile, die diesen Zeiger auf Null setzt, redundant ist.

Eine weitere Gefahr besteht darin, sich bei Ausnahmen mit Code auf diese Technik zu verlassen:

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

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

In einem solchen Code erzeugen Sie entweder ein 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 sicherlich mehr erzählen), werfen für mich alle Fragen der Art "Wie vermeide ich die Verwendung von Smart-Pointern auf und erledige die Arbeit sicher mit normalen Zeigern" als veraltet auf.


Ich verstehe nicht, wie der 4-Liner wesentlich komplexer ist als ein 3-Liner (man sollte sowieso lock_guards verwenden) und wenn Ihr Destruktor wirft, sind Sie trotzdem in Schwierigkeiten.
MikeMB

Als ich diese Antwort zum ersten Mal sah, verstand ich nicht, warum Sie einen Zeiger in einem Destruktor auf Null setzen möchten, aber jetzt tue ich das - für den Fall, dass das Objekt , dem der Zeiger gehört, nach dem Löschen verwendet wird!
Mark Ransom


0

Wenn Sie den Zeiger neu zuweisen möchten, bevor Sie ihn erneut verwenden (Dereferenzieren, Übergeben an eine Funktion usw.), ist es nur eine zusätzliche Operation, den Zeiger auf NULL zu setzen. Wenn Sie 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 in dieser früheren Antwort sagte, muss beim Löschen eines Zeigers in einem Destruktor kein NULL zugewiesen werden, da er nicht erneut verwendet wird, da das Objekt bereits zerstört wird.


0

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


0

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 tr1 (std :: tr1-Namespace, #include <speicher>) oder wenn Ihr Compiler es nicht bereitstellt, holen Sie es von boost.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.