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?
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?
Antworten:
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 .
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;
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.
}
Zusammenfassend (nach dem Kommentar von Ogre Psalm33 ) stützt sich RAII auf drei Konzepte:
Dies bedeutet, dass im richtigen C ++ - Code die meisten Objekte nicht mit erstellt new
werden 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
/ finally
für diesen Fall intensiv nutzen ) ...
"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 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 terminate
oder exit
in 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) .
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 ...
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:
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.
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!
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.
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.
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:
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.)
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.
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
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.
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
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.
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.
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.
valgrind (nur für * nix-Plattformen verfügbar) ist eine sehr schöne Speicherprüfung
Wenn Sie Ihren Speicher manuell verwalten möchten, haben Sie zwei Fälle:
Wenn Sie gegen eine dieser Regeln verstoßen müssen, dokumentieren Sie diese bitte.
Es geht um Zeigerbesitz.
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.
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.
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.
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!
Verwalten Sie den Speicher genauso wie andere Ressourcen (Handles, Dateien, Datenbankverbindungen, Sockets ...). GC würde Ihnen auch nicht dabei helfen.
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.