Allgemeine Richtlinien zur Vermeidung von Speicherlecks in C ++ [geschlossen]


130

Was sind einige allgemeine Tipps, um sicherzustellen, dass in C ++ - Programmen kein Speicher verloren geht? Wie finde ich heraus, wer den dynamisch zugewiesenen Speicher freigeben soll?


26
Scheint mir ziemlich konstruktiv.
Shoerob

11
Das ist konstruktiv. Und die Antworten werden durch Fakten, Fachwissen, Referenzen usw. unterstützt. Und sehen Sie die Anzahl der Upvotes / Antworten. !!
Samitha Chathuranga

Antworten:


40

Versuchen Sie, den Speicher nicht manuell zu verwalten, sondern gegebenenfalls intelligente Zeiger zu verwenden.
Schauen Sie sich die Boost lib , TR1 und Smart Pointer an .
Auch intelligente Zeiger sind jetzt Teil des C ++ - Standards C ++ 11 .


1
Um mit g ++ zu kompilieren, muss man param hinzufügen: -std = c ++ 0x
Paweł Szczur

oder Sie können mit g ++ unter Verwendung des Flag-Wertes -std = c ++ 11
Prabhash Rathore

200

Ich unterstütze alle Ratschläge zu RAII und intelligenten Zeigern gründlich, möchte aber auch einen etwas übergeordneten Tipp hinzufügen: Der am einfachsten zu verwaltende Speicher ist der Speicher, den Sie nie zugewiesen haben. Im Gegensatz zu Sprachen wie C # und Java, in denen so ziemlich alles eine Referenz ist, sollten Sie in C ++ Objekte auf den Stapel legen, wann immer Sie können. Wie ich gesehen habe, weisen mehrere Leute (einschließlich Dr. Stroustrup) darauf hin, dass der Hauptgrund, warum die Speicherbereinigung in C ++ nie populär war, darin besteht, dass gut geschriebenes C ++ überhaupt nicht viel Müll produziert.

Schreib nicht

Object* x = new Object;

oder auch

shared_ptr<Object> x(new Object);

wenn du nur schreiben kannst

Object x;

34
Ich wünschte, ich könnte dies mit +10 bewerten. Dies ist das größte Problem, das ich heute bei den meisten C ++ - Programmierern sehe, und ich gehe davon aus, dass sie Java vor C ++ gelernt haben.
Kristopher Johnson

Sehr interessanter Punkt - Ich hatte mich gefragt, warum ich so viel seltener Probleme mit der C ++ - Speicherverwaltung habe als in anderen Sprachen, aber jetzt verstehe ich, warum: Es erlaubt tatsächlich, dass
Dinge

Was machen Sie also, wenn Sie Objekt x schreiben? und dann x wegwerfen wollen? Angenommen, x wurde in der Hauptmethode erstellt.
Yamcha

3
@ user1316459 Mit C ++ können Sie Bereiche auch im laufenden Betrieb erstellen. Alles, was Sie tun müssen, ist, die Lebensdauer von x in Klammern wie folgt zu setzen: {Objekt x; x.DoSomething; }. Nach dem letzten '}' wird der Destruktor von x aufgerufen, um alle darin enthaltenen Ressourcen freizugeben. Wenn x selbst der Speicher ist, der auf dem Heap zugewiesen werden soll, empfehle ich, ihn in einen unique_ptr zu verpacken, damit er einfach und angemessen bereinigt wird.
David Peterson

1
Robert: Ja. Ross sagte nicht "Schreibe niemals [Code, der Neues enthält]", er sagte "Schreibe [das] nicht, wenn du es einfach [auf den Stapel legen ] kannst ". Große Objekte auf dem Heap sind in den meisten Situationen weiterhin der richtige Aufruf, insbesondere für leistungsintensiven Code.
Codetaku

104

Verwenden Sie RAII

  • Vergessen Sie die Garbage Collection (verwenden Sie stattdessen RAII). Beachten Sie, dass auch der Garbage Collector auslaufen kann (wenn Sie vergessen, einige Referenzen in Java / C # auf "null" zu setzen), und dass Garbage Collector Ihnen nicht dabei hilft, Ressourcen zu entsorgen (wenn Sie ein Objekt haben, für das ein Handle erworben wurde Bei einer Datei wird die Datei nicht automatisch freigegeben, wenn das Objekt den Gültigkeitsbereich verlässt, wenn Sie dies nicht manuell in Java tun oder das Muster "dispose" in C # verwenden.
  • Vergessen Sie die Regel "eine Rückgabe pro Funktion" . Dies ist ein guter C-Rat, um Lecks zu vermeiden. In C ++ ist er jedoch aufgrund der Verwendung von Ausnahmen veraltet (verwenden Sie stattdessen RAII).
  • Und während das "Sandwich-Muster" ein guter C-Rat ist, ist es in C ++ aufgrund der Verwendung von Ausnahmen veraltet (verwenden Sie stattdessen RAII).

Dieser Beitrag scheint sich zu wiederholen, aber in C ++ ist RAII das grundlegendste Muster, das man kennen muss .

Erfahren Sie, wie Sie intelligente Zeiger verwenden, sowohl von Boost über TR1 als auch von Auto_ptr (aber häufig effizient genug) (aber Sie müssen die Einschränkungen kennen).

RAII ist die Grundlage sowohl für die Ausnahmesicherheit als auch für die Ressourcenentsorgung in C ++, und kein anderes Muster (Sandwich usw.) gibt Ihnen beides (und meistens gibt es Ihnen keines).

Unten sehen Sie einen Vergleich von RAII- und Nicht-RAII-Code:

void doSandwich()
{
   T * p = new T() ;
   // do something with p
   delete p ; // leak if the p processing throws or return
}

void doRAIIDynamic()
{
   std::auto_ptr<T> p(new T()) ; // you can use other smart pointers, too
   // do something with p
   // WON'T EVER LEAK, even in case of exceptions, returns, breaks, etc.
}

void doRAIIStatic()
{
   T p ;
   // do something with p
   // WON'T EVER LEAK, even in case of exceptions, returns, breaks, etc.
}

Über RAII

Zusammenfassend (nach dem Kommentar von Ogre Psalm33 ) stützt sich RAII auf drei Konzepte:

  • Sobald das Objekt erstellt ist, funktioniert es einfach! Erwerben Sie Ressourcen im Konstruktor.
  • Objektzerstörung ist genug! Machen Sie freie Ressourcen im Destruktor.
  • Alles dreht sich um Bereiche! Objekte mit Gültigkeitsbereich (siehe doRAIIStatic-Beispiel oben) werden bei ihrer Deklaration erstellt und in dem Moment zerstört, in dem die Ausführung den Gültigkeitsbereich verlässt, unabhängig davon, wie der Exit (Rückgabe, Unterbrechung, Ausnahme usw.) erfolgt.

Dies bedeutet, dass im richtigen C ++ - Code die meisten Objekte nicht mit erstellt newwerden und stattdessen auf dem Stapel deklariert werden. Und für diejenigen, die mit konstruiert wurden new, sind alle irgendwie begrenzt (z. B. an einen intelligenten Zeiger angehängt).

Als Entwickler ist dies in der Tat sehr leistungsfähig, da Sie sich nicht um die manuelle Handhabung von Ressourcen kümmern müssen (wie in C oder für einige Objekte in Java, die try/ finallyfür diesen Fall intensiv nutzen ) ...

Bearbeiten (2012-02-12)

"Objekte mit Gültigkeitsbereich ... werden zerstört ... unabhängig vom Ausgang", das ist nicht ganz richtig. Es gibt Möglichkeiten, RAII zu betrügen. Jede Variante von terminate () umgeht die Bereinigung. exit (EXIT_SUCCESS) ist in dieser Hinsicht ein Oxymoron.

- wilhelmtell

wilhelmtell hat damit recht: Es gibt außergewöhnliche Möglichkeiten, RAII zu betrügen, die alle dazu führen, dass der Prozess abrupt gestoppt wird.

Dies sind außergewöhnliche Möglichkeiten, da C ++ - Code nicht mit Beenden, Beenden usw. übersät ist. Im Falle von Ausnahmen möchten wir, dass eine nicht behandelte Ausnahme den Prozess zum Absturz bringt und das Speicherabbild unverändert und nicht nach der Bereinigung ausgibt .

Aber wir müssen immer noch über diese Fälle Bescheid wissen, denn obwohl sie selten auftreten, können sie dennoch auftreten.

(Wer ruft an terminateoder exitin gelegentlichem C ++ - Code? ... Ich erinnere mich, dass ich mich beim Spielen mit GLUT mit diesem Problem befassen musste : Diese Bibliothek ist sehr C-orientiert und geht so weit, sie aktiv zu entwerfen, um es C ++ - Entwicklern zu erschweren, sich nicht darum zu kümmern über vom Stapel zugewiesene Daten oder über "interessante" Entscheidungen, niemals von ihrer Hauptschleife zurückzukehren ... dazu werde ich nichts sagen) .


Muss die T-Klasse nicht RAII verwenden, um sicherzustellen, dass doRAIIStatic () keinen Speicher verliert? Zum Beispiel T p (); p.doSandwich (); Ich weiß allerdings nicht viel darüber.
Daniel O

@ Ogre Psalm33: Danke für den Kommentar. Natürlich hast du recht. Ich habe beide Links zur RAII-Wikipedia-Seite und eine kleine Zusammenfassung von RAII hinzugefügt.
Paercebal

1
@Shiftbit: Drei Möglichkeiten, in der Reihenfolge ihrer Präferenz: _ _ _ 1. Legen Sie ein reales Objekt in den STL-Container. _ _ _ 2. Platzieren Sie intelligente Zeiger (shared_ptr) von Objekten im STL-Container. _ _ _ 3. Fügen Sie Rohzeiger in den STL-Container ein, wickeln Sie den Container jedoch ein, um den Zugriff auf die Daten zu steuern. Der Wrapper stellt sicher, dass der Destruktor die zugewiesenen Objekte freigibt, und die Wrapper-Accessoren stellen sicher, dass beim Zugriff auf / Ändern des Containers nichts beschädigt wird.
Paercebal

1
@Robert: In C ++ 03 würden Sie doRAIIDynamic in einer Funktion verwenden, die entweder einer untergeordneten oder einer übergeordneten Funktion (oder einem globalen Bereich) den Besitz erteilen muss. Oder wenn Sie über eine Factory eine Schnittstelle zu einem polymorphen Objekt erhalten (wenn ein korrekter Zeiger einen intelligenten Zeiger zurückgibt). In C ++ 11 ist dies weniger der Fall, da Sie Ihr Objekt beweglich machen können, so dass es einfacher ist, den Besitz eines auf dem Stapel deklarierten Objekts zu
erteilen

2
@Robert: ... Beachten Sie, dass das Deklarieren eines Objekts auf dem Stapel nicht bedeutet, dass das Objekt den Heap nicht intern verwendet (beachten Sie die doppelte Negation ... :-) ...). Beispielsweise verfügt std :: string, das mit Small String Optimization implementiert wurde, über einen Puffer "auf dem Stapel der Klasse" für kleine Zeichenfolgen (~ 15 Zeichen) und verwendet für größere Zeichenfolgen einen Zeiger auf einen Speicher im Heap ... Von außen gesehen ist std :: string jedoch immer noch ein Werttyp, den Sie (normalerweise) auf dem Stapel deklarieren und den Sie wie eine Ganzzahl verwenden (im Gegensatz zu: wie Sie eine Schnittstelle für eine polymorphe Klasse verwenden würden).
Paercebal

25

Sie sollten sich intelligente Zeiger ansehen, z. B. die intelligenten Zeiger von boost .

Anstatt

int main()
{ 
    Object* obj = new Object();
    //...
    delete obj;
}

boost :: shared_ptr wird automatisch gelöscht, sobald der Referenzzähler Null ist:

int main()
{
    boost::shared_ptr<Object> obj(new Object());
    //...
    // destructor destroys when reference count is zero
}

Beachten Sie meine letzte Anmerkung: "Wenn die Referenzanzahl Null ist, ist dies der coolste Teil. Wenn Sie also mehrere Benutzer Ihres Objekts haben, müssen Sie nicht nachverfolgen, ob das Objekt noch verwendet wird. Sobald sich niemand mehr auf Ihr Objekt bezieht." geteilter Zeiger, es wird zerstört.

Dies ist jedoch kein Allheilmittel. Obwohl Sie auf den Basiszeiger zugreifen können, möchten Sie ihn nicht an eine Drittanbieter-API übergeben, es sei denn, Sie waren sich sicher, was er tat. Oftmals werden Ihre "Posting" -Stücke in einem anderen Thread veröffentlicht, damit die Arbeit erledigt werden kann, nachdem der Erstellungsbereich abgeschlossen ist. Dies ist bei PostThreadMessage in Win32 üblich:

void foo()
{
   boost::shared_ptr<Object> obj(new Object()); 

   // Simplified here
   PostThreadMessage(...., (LPARAM)ob.get());
   // Destructor destroys! pointer sent to PostThreadMessage is invalid! Zohnoes!
}

Verwenden Sie wie immer Ihre Denkmütze mit jedem Werkzeug ...


12

Informieren Sie sich über RAII und stellen Sie sicher, dass Sie es verstehen.


11

Die meisten Speicherverluste sind darauf zurückzuführen, dass der Besitz und die Lebensdauer von Objekten nicht klar sind.

Das erste, was Sie tun müssen, ist, den Stapel zuzuweisen, wann immer Sie können. Dies betrifft die meisten Fälle, in denen Sie ein einzelnes Objekt für einen bestimmten Zweck zuweisen müssen.

Wenn Sie ein Objekt "neu" machen müssen, hat es die meiste Zeit für den Rest seines Lebens einen einzigen offensichtlichen Besitzer. In dieser Situation verwende ich in der Regel eine Reihe von Sammlungsvorlagen, die dazu bestimmt sind, Objekte zu besitzen, die per Zeiger in ihnen gespeichert sind. Sie werden mit dem STL-Vektor und den Kartencontainern implementiert, weisen jedoch einige Unterschiede auf:

  • Diese Sammlungen können nicht kopiert oder zugewiesen werden. (Sobald sie Objekte enthalten.)
  • In sie werden Zeiger auf Objekte eingefügt.
  • Wenn die Sammlung gelöscht wird, wird der Destruktor zuerst für alle Objekte in der Sammlung aufgerufen. (Ich habe eine andere Version, in der behauptet wird, dass sie zerstört und nicht leer ist.)
  • Da sie Zeiger speichern, können Sie auch geerbte Objekte in diesen Containern speichern.

Mein Vorteil bei STL ist, dass es sich so stark auf Wertobjekte konzentriert, während Objekte in den meisten Anwendungen eindeutige Entitäten sind, für deren Verwendung in diesen Containern keine aussagekräftige Kopiersemantik erforderlich ist.


10

Bah, du kleine Kinder und deine neuen Müllsammler ...

Sehr strenge Regeln zum "Eigentum" - welches Objekt oder welcher Teil der Software hat das Recht, das Objekt zu löschen. Klare Kommentare und kluge Variablennamen, um deutlich zu machen, ob ein Zeiger "besitzt" oder "nur schauen, nicht berühren" ist. Um zu entscheiden, wem was gehört, befolgen Sie so weit wie möglich das "Sandwich" -Muster in jeder Unterroutine oder Methode.

create a thing
use that thing
destroy that thing

Manchmal ist es notwendig, an sehr unterschiedlichen Orten zu erschaffen und zu zerstören. Ich denke schwer, das zu vermeiden.

In jedem Programm, das komplexe Datenstrukturen erfordert, erstelle ich einen strengen Baum von Objekten, die andere Objekte enthalten - unter Verwendung von "Eigentümer" -Zeigern. Dieser Baum modelliert die grundlegende Hierarchie von Anwendungsdomänenkonzepten. Beispiel: Eine 3D-Szene besitzt Objekte, Lichter und Texturen. Am Ende des Renderns, wenn das Programm beendet wird, gibt es eine klare Möglichkeit, alles zu zerstören.

Viele andere Zeiger werden nach Bedarf definiert, wenn eine Entität Zugriff auf eine andere benötigt, um über Arays oder was auch immer zu scannen. das sind die "nur schauen". Für das Beispiel einer 3D-Szene verwendet ein Objekt eine Textur, besitzt diese jedoch nicht. Andere Objekte verwenden möglicherweise dieselbe Textur. Die Zerstörung eines Objekts führt nicht zur Zerstörung von Texturen.

Ja, es ist zeitaufwändig, aber genau das mache ich. Ich habe selten Speicherlecks oder andere Probleme. Aber dann arbeite ich auf dem begrenzten Gebiet der Hochleistungssoftware für Wissenschaft, Datenerfassung und Grafik. Ich beschäftige mich nicht oft mit Transaktionen wie Bank- und E-Commerce-Transaktionen, ereignisgesteuerten GUIs oder stark vernetztem asynchronem Chaos. Vielleicht haben die neuen Wege dort einen Vorteil!


Ich stimme voll und ganz zu. Wenn Sie in einer eingebetteten Umgebung arbeiten, haben Sie möglicherweise auch nicht den Luxus von Bibliotheken von Drittanbietern.
Simon

6
Ich bin nicht einverstanden. Wenn im Teil "Verwenden Sie das Ding" eine Rückgabe oder eine Ausnahme ausgelöst wird, verpassen Sie die Freigabe. Was die Leistung betrifft, würde std :: auto_ptr Sie nichts kosten. Nicht, dass ich niemals so codiere wie Sie. Es gibt nur einen Unterschied zwischen 100% und 99% sicherem Code. :-)
paercebal

8

Gute Frage!

Wenn Sie C ++ verwenden und eine Echtzeit-CPU- und Speicher-Boud-Anwendung (wie Spiele) entwickeln, müssen Sie Ihren eigenen Speichermanager schreiben.

Ich denke, das Bessere, was Sie tun können, ist, einige interessante Werke verschiedener Autoren zusammenzuführen. Ich kann Ihnen einen Hinweis geben:

  • Der Allokator mit fester Größe wird überall im Netz stark diskutiert

  • Small Object Allocation wurde 2001 von Alexandrescu in seinem perfekten Buch "Modern c ++ design" eingeführt.

  • Eine große Weiterentwicklung (mit verteiltem Quellcode) findet sich in einem erstaunlichen Artikel in Game Programming Gem 7 (2008) mit dem Titel "High Performance Heap Allocator" von Dimitar Lazarov

  • Eine große Liste von Ressourcen finden Sie in diesem Artikel

Schreiben Sie nicht selbst einen unbenutzenden Noob-Allokator ... DOKUMENTIEREN SIE SICH zuerst.


5

Eine Technik, die bei der Speicherverwaltung in C ++ populär geworden ist, ist RAII . Grundsätzlich verwenden Sie Konstruktoren / Destruktoren, um die Ressourcenzuweisung zu handhaben. Natürlich gibt es in C ++ aufgrund der Ausnahmesicherheit einige andere unangenehme Details, aber die Grundidee ist ziemlich einfach.

Das Problem hängt im Allgemeinen vom Eigentum ab. Ich empfehle dringend, die Effective C ++ - Reihe von Scott Meyers und Modern C ++ Design von Andrei Alexandrescu zu lesen.


5

Es gibt bereits eine Menge darüber, wie man keine Leckagen verursacht. Wenn Sie jedoch ein Tool benötigen, mit dem Sie Leckagen nachverfolgen können, schauen Sie sich Folgendes an:


BoundsChecker ist 404ing.
TankorSmash

4

Benutzer intelligente Zeiger überall, wo Sie können! Ganze Klassen von Speicherlecks verschwinden einfach.


4

Teilen und kennen Sie die Regeln für den Speicherbesitz in Ihrem Projekt. Die Verwendung der COM-Regeln sorgt für die beste Konsistenz ([in] -Parameter gehören dem Anrufer, Angerufene müssen kopieren; [out] -Parameter gehören dem Anrufer, Angerufene müssen eine Kopie erstellen, wenn eine Referenz aufbewahrt wird; usw.)


4

valgrind ist ein gutes Werkzeug, um Speicherverluste Ihres Programms auch zur Laufzeit zu überprüfen.

Es ist auf den meisten Linux-Versionen (einschließlich Android) und auf Darwin verfügbar.

Wenn Sie Unit-Tests für Ihre Programme schreiben, sollten Sie es sich zur Gewohnheit machen, Valgrind systematisch für Tests auszuführen. Dadurch werden möglicherweise frühzeitig viele Speicherverluste vermieden. Es ist normalerweise auch einfacher, sie in einfachen Tests zu lokalisieren, als in einer vollständigen Software.

Natürlich bleibt dieser Rat für jedes andere Tool zur Speicherprüfung gültig.


3

Verwenden Sie auch keinen manuell zugewiesenen Speicher, wenn eine Standardbibliotheksklasse (z. B. ein Vektor) vorhanden ist. Stellen Sie sicher, dass Sie einen virtuellen Destruktor haben, wenn Sie gegen diese Regel verstoßen.


2

Wenn Sie für etwas keinen intelligenten Zeiger verwenden können (obwohl dies eine große rote Fahne sein sollte), geben Sie Ihren Code ein mit:

allocate
if allocation succeeded:
{ //scope)
     deallocate()
}

Das ist offensichtlich, aber stellen Sie sicher, dass Sie es eingeben, bevor Sie Code in den Bereich eingeben


2

Eine häufige Ursache für diese Fehler ist, wenn Sie über eine Methode verfügen, die einen Verweis oder Zeiger auf ein Objekt akzeptiert, den Besitz jedoch unklar lässt. Stil- und Kommentarkonventionen können dies weniger wahrscheinlich machen.

Der Fall, in dem die Funktion das Eigentum an dem Objekt übernimmt, sei der Sonderfall. Schreiben Sie in allen Situationen, in denen dies geschieht, einen Kommentar neben die Funktion in die Header-Datei, der dies angibt. Sie sollten sich bemühen, sicherzustellen, dass in den meisten Fällen das Modul oder die Klasse, die ein Objekt zuweist, auch für die Freigabe verantwortlich ist.

Die Verwendung von const kann in einigen Fällen sehr hilfreich sein. Wenn eine Funktion ein Objekt nicht ändert und keinen Verweis darauf speichert, der nach seiner Rückkehr bestehen bleibt, akzeptieren Sie einen konstanten Verweis. Aus dem Lesen des Anrufercodes geht hervor, dass Ihre Funktion das Eigentum an dem Objekt nicht akzeptiert hat. Sie hätten dieselbe Funktion einen Nicht-Konstanten-Zeiger akzeptieren lassen können, und der Aufrufer könnte angenommen haben oder nicht, dass der Angerufene den Besitz angenommen hat, aber mit einer Konstanten-Referenz gibt es keine Frage.

Verwenden Sie keine nicht konstanten Referenzen in Argumentlisten. Beim Lesen des Anrufercodes ist sehr unklar, ob der Angerufene möglicherweise einen Verweis auf den Parameter beibehalten hat.

Ich bin mit den Kommentaren nicht einverstanden, in denen Zeiger mit Referenzzählung empfohlen werden. Dies funktioniert normalerweise gut, aber wenn Sie einen Fehler haben und es nicht funktioniert, insbesondere wenn Ihr Destruktor etwas nicht Triviales tut, wie in einem Multithread-Programm. Versuchen Sie auf jeden Fall, Ihr Design so anzupassen, dass keine Referenzzählung erforderlich ist, wenn es nicht zu schwierig ist.


2

Tipps in der Reihenfolge ihrer Wichtigkeit:

-Tipp Nr. 1 Denken Sie immer daran, Ihre Destruktoren als "virtuell" zu deklarieren.

-Tipp Nr. 2 Verwenden Sie RAII

-Tipp Nr. 3 Verwenden Sie die Smartpointers von Boost

-Tipp Nr. 4 Schreiben Sie keine eigenen fehlerhaften Smartpointers, verwenden Sie Boost (bei einem Projekt, an dem ich gerade arbeite, kann ich Boost nicht verwenden, und ich musste meine eigenen Smartpointer debuggen, die ich definitiv nicht nehmen würde wieder die gleiche Route, aber im Moment kann ich unseren Abhängigkeiten keinen Schub hinzufügen)

-Tipp Nr. 5 Wenn es um gelegentliche / nicht leistungskritische Arbeiten geht (wie bei Spielen mit Tausenden von Objekten), schauen Sie sich den Boost-Zeiger-Container von Thorsten Ottosen an

-Tipp Nr. 6 Suchen Sie einen Leckerkennungs-Header für die Plattform Ihrer Wahl, z. B. den "vld" -Header von Visual Leak Detection


Ich vermisse vielleicht einen Trick, aber wie können "Spiel" und "nicht leistungskritisch" im selben Satz sein?
Adam Naylor

Spiele sind natürlich ein Beispiel für das kritische Szenario.
Robert Gould

Tipp 1 sollte nur angewendet werden, wenn die Klasse über mindestens eine virtuelle Methode verfügt. Ich würde einer Klasse, die nicht als Basisklasse in einem polymorphen Vererbungsbaum dienen soll, niemals einen nutzlosen virtuellen Destruktor auferlegen.
antred

1

Wenn Sie können, verwenden Sie boost shared_ptr und Standard C ++ auto_ptr. Diese vermitteln Besitzersemantik.

Wenn Sie ein auto_ptr zurückgeben, teilen Sie dem Anrufer mit, dass Sie ihm den Besitz des Speichers übertragen.

Wenn Sie einen shared_ptr zurückgeben, teilen Sie dem Anrufer mit, dass Sie einen Verweis darauf haben und er Teil des Eigentums ist, aber es liegt nicht nur in seiner Verantwortung.

Diese Semantik gilt auch für Parameter. Wenn der Anrufer Ihnen ein auto_ptr übergibt, gibt er Ihnen das Eigentum.


1

Andere haben Möglichkeiten erwähnt, Speicherlecks zu vermeiden (wie intelligente Zeiger). Ein Tool zur Profilerstellung und Speicheranalyse ist jedoch häufig die einzige Möglichkeit, Speicherprobleme aufzuspüren, sobald Sie sie haben.

Valgrind Memcheck ist ein ausgezeichneter kostenloser.


1

Fügen Sie nur für MSVC am Anfang jeder CPP-Datei Folgendes hinzu:

#ifdef _DEBUG
#define new DEBUG_NEW
#endif

Wenn Sie dann mit VS2003 oder höher debuggen, werden Sie beim Beenden Ihres Programms über eventuelle Lecks informiert (es verfolgt Neu / Löschen). Es ist einfach, aber es hat mir in der Vergangenheit geholfen.


1

valgrind (nur für * nix-Plattformen verfügbar) ist eine sehr schöne Speicherprüfung


1

Wenn Sie Ihren Speicher manuell verwalten möchten, haben Sie zwei Fälle:

  1. Ich habe das Objekt erstellt (möglicherweise indirekt durch Aufrufen einer Funktion, die ein neues Objekt zuweist), ich verwende es (oder eine von mir aufgerufene Funktion verwendet es) und gebe es dann frei.
  2. Jemand gab mir die Referenz, also sollte ich sie nicht befreien.

Wenn Sie gegen eine dieser Regeln verstoßen müssen, dokumentieren Sie diese bitte.

Es geht um Zeigerbesitz.


1
  • Vermeiden Sie die dynamische Zuordnung von Objekten. Solange Klassen über geeignete Konstruktoren und Destruktoren verfügen, verwenden Sie eine Variable des Klassentyps und keinen Zeiger darauf. Sie vermeiden die dynamische Zuweisung und Freigabe, da der Compiler dies für Sie erledigt.
    Eigentlich ist das auch der Mechanismus, der von "Smart Pointern" verwendet wird und von einigen anderen Autoren als RAII bezeichnet wird ;-).
  • Wenn Sie Objekte an andere Funktionen übergeben, ziehen Sie Referenzparameter Zeigern vor. Dies vermeidet einige mögliche Fehler.
  • Deklarieren Sie nach Möglichkeit Parameter const, insbesondere Zeiger auf Objekte. Auf diese Weise können Objekte nicht "versehentlich" freigegeben werden (außer wenn Sie die Konstante wegwerfen ;-))).
  • Minimieren Sie die Anzahl der Stellen im Programm, an denen Sie Speicherzuweisung und Freigabe vornehmen. Z.B. Wenn Sie denselben Typ mehrmals zuweisen oder freigeben, schreiben Sie eine Funktion dafür (oder eine Factory-Methode ;-)).
    Auf diese Weise können Sie bei Bedarf auf einfache Weise eine Debug-Ausgabe erstellen (welche Adressen zugewiesen und freigegeben werden, ...).
  • Verwenden Sie eine Factory-Funktion, um Objekte mehrerer verwandter Klassen einer einzigen Funktion zuzuweisen.
  • Wenn Ihre Klassen eine gemeinsame Basisklasse mit einem virtuellen Destruktor haben, können Sie alle mit derselben Funktion (oder statischen Methode) freigeben.
  • Überprüfen Sie Ihr Programm mit Tools wie purify (leider viele $ / € / ...).

0

Sie können die Speicherzuweisungsfunktionen abfangen und feststellen, ob beim Beenden des Programms einige Speicherzonen nicht freigegeben wurden (obwohl dies nicht für alle Anwendungen geeignet ist ).

Dies kann auch zur Kompilierungszeit erfolgen, indem die Operatoren new und delete sowie andere Speicherzuweisungsfunktionen ersetzt werden.

Beispiel: Überprüfen Sie diese Site [Debuggen der Speicherzuordnung in C ++]. Hinweis: Es gibt einen Trick für den Löschoperator, der auch so aussieht:

#define DEBUG_DELETE PrepareDelete(__LINE__,__FILE__); delete
#define delete DEBUG_DELETE

Sie können in einigen Variablen den Namen der Datei speichern und wann der überladene Löschoperator weiß, von welchem ​​Ort aus er aufgerufen wurde. Auf diese Weise können Sie die Verfolgung aller Löschvorgänge und Mallocs aus Ihrem Programm abrufen. Am Ende der Speicherüberprüfungssequenz sollten Sie in der Lage sein zu melden, welcher zugewiesene Speicherblock nicht "gelöscht" wurde, und ihn anhand des Dateinamens und der Zeilennummer identifizieren, was ich denke, was Sie wollen.

Sie können auch etwas wie BoundsChecker unter Visual Studio ausprobieren, das ziemlich interessant und einfach zu bedienen ist.


0

Wir verpacken alle unsere Zuordnungsfunktionen mit einer Ebene, die vorne eine kurze Zeichenfolge und am Ende ein Sentinel-Flag anfügt. So würden Sie beispielsweise "myalloc (pszSomeString, iSize, iAlignment)" oder "new (" description ", iSize) MyObject ()" aufrufen, das intern die angegebene Größe und genügend Speicherplatz für Ihren Header und Sentinel zuweist. Natürlich Vergessen Sie nicht, dies für Nicht-Debug-Builds zu kommentieren! Dies erfordert etwas mehr Speicher, aber die Vorteile überwiegen bei weitem die Kosten.

Dies hat drei Vorteile: Erstens können Sie einfach und schnell nachverfolgen, welcher Code ausläuft, indem Sie schnell nach Code suchen, der in bestimmten "Zonen" zugewiesen, aber nicht bereinigt wurde, wenn diese Zonen freigegeben werden sollten. Es kann auch nützlich sein, zu erkennen, wann eine Grenze überschrieben wurde, indem überprüft wird, ob alle Sentinels intakt sind. Dies hat uns viele Male erspart, als wir versucht haben, diese gut versteckten Abstürze oder Array-Fehltritte zu finden. Der dritte Vorteil besteht darin, die Verwendung des Speichers zu verfolgen, um festzustellen, wer die großen Player sind. Eine Zusammenstellung bestimmter Beschreibungen in einem MemDump zeigt Ihnen beispielsweise, wann „Sound“ viel mehr Platz beansprucht, als Sie erwartet haben.


0

C ++ wurde unter Berücksichtigung von RAII entwickelt. Es gibt wirklich keinen besseren Weg, um Speicher in C ++ zu verwalten, denke ich. Achten Sie jedoch darauf, dem lokalen Bereich keine sehr großen Blöcke (wie Pufferobjekte) zuzuweisen. Dies kann zu Stapelüberläufen führen. Wenn bei der Überprüfung der Grenzen während der Verwendung dieses Blocks ein Fehler auftritt, können Sie andere Variablen überschreiben oder Adressen zurückgeben, was zu Sicherheitslücken aller Art führt.


0

Eines der wenigen Beispiele für das Zuweisen und Zerstören an verschiedenen Orten ist die Thread-Erstellung (der Parameter, den Sie übergeben). Aber auch in diesem Fall ist es einfach. Hier ist die Funktion / Methode, die einen Thread erstellt:

struct myparams {
int x;
std::vector<double> z;
}

std::auto_ptr<myparams> param(new myparams(x, ...));
// Release the ownership in case thread creation is successfull
if (0 == pthread_create(&th, NULL, th_func, param.get()) param.release();
...

Hier stattdessen die Thread-Funktion

extern "C" void* th_func(void* p) {
   try {
       std::auto_ptr<myparams> param((myparams*)p);
       ...
   } catch(...) {
   }
   return 0;
}

Ziemlich einfach, nicht wahr? Falls die Thread-Erstellung fehlschlägt, wird die Ressource vom auto_ptr freigegeben (gelöscht), andernfalls wird der Besitz an den Thread übergeben. Was ist, wenn der Thread so schnell ist, dass er nach der Erstellung die Ressource vor dem freigibt?

param.release();

wird in der Hauptfunktion / Methode aufgerufen? Nichts! Weil wir dem auto_ptr sagen, dass es die Freigabe ignorieren soll. Ist die C ++ - Speicherverwaltung einfach? Prost,

Ema!


0

Verwalten Sie den Speicher genauso wie andere Ressourcen (Handles, Dateien, Datenbankverbindungen, Sockets ...). GC würde Ihnen auch nicht dabei helfen.


-3

Genau eine Rückkehr von jeder Funktion. Auf diese Weise können Sie dort eine Freigabe vornehmen und diese nie verpassen.

Ansonsten ist es zu leicht, einen Fehler zu machen:

new a()
if (Bad()) {delete a; return;}
new b()
if (Bad()) {delete a; delete b; return;}
... // etc.

Ihre Antwort stimmt hier nicht mit dem Beispielcode überein? Ich stimme der Antwort "nur eine Rückgabe" zu, aber der Beispielcode zeigt, was NICHT zu tun ist.
Simon

1
Bei C ++ RAII geht es genau darum, die Art von Code zu vermeiden, die Sie geschrieben haben. In C ist dies wahrscheinlich das Richtige. In C ++ ist Ihr Code jedoch fehlerhaft. Zum Beispiel: Was ist, wenn neues b () wirft? Sie lecken a.
Paercebal
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.