2009-03-28 9 views
5

Ich arbeite derzeit an einer C-basierte Anwendung bin ein bisschen fest auf Freigeben von Speicher in einer nicht-antipattern Mode. Ich bin ein Gedächtnis-Management-Amateur.Muster zum Freigeben von Speicher in C?

Mein Hauptproblem ist, ich erkläre Speicherstrukturen in verschiedenen Bereichen, und diese Strukturen werden durch Verweis auf andere Funktionen weitergegeben. Einige dieser Funktionen können Fehler und exit() auslösen.

Wie beziehe ich meine Strukturen frei, wenn ich() in einem Bereich verlasse, aber nicht alle meine Datenstrukturen in diesem Bereich liegen?

Ich habe das Gefühl, ich muss alles in eine psuedo Ausnahmebehandler wickeln und die Handler beschäftigen sich mit Befreien, aber das scheint immer noch hässlich, weil es über alles wissen müsste ich oder nicht brauchen, um zu befreien. ..

Antwort

3

Betrachten Sie Wrapper zu malloc und verwenden Sie sie in einer disziplinierten Weise. Verfolgen Sie den Speicher, den Sie zuweisen (möglicherweise in einer verknüpften Liste), und verwenden Sie einen Wrapper zum Beenden, um Ihren Speicher aufzuzählen, um ihn freizugeben. Sie könnte auch den Speicher mit einem zusätzlichen Parameter und Mitglied Ihrer verknüpften Listenstruktur benennen. In Anwendungen, in denen der zugewiesene Speicher sehr vom Umfang abhängig ist, werden Sie feststellen, dass Speicher ausgelaufen ist. Dies kann eine gute Methode sein, um den Speicher zu entladen und zu analysieren.

UPDATE: Threading in Ihrer Anwendung wird dies sehr komplex. Siehe andere Antworten zu Threading-Problemen.

+0

Sie könnten sogar die Funktion atexit() verwenden und eine Funktion schreiben, um den gesamten auf der verknüpften Liste zugewiesenen Speicher (der in diesem Fall eine globale Variable sein müsste) nach einem einfachen Aufruf von exit() freizugeben - so würden Sie es tun Ich darf nicht vergessen, keinen einfachen Ausgang zu verwenden(). –

+0

Aber warum? Das Betriebssystem führt bereits eine Liste des zugewiesenen Speichers. Duplizierst du diese Funktionalität, bremst du den Shut-Down-Prozess einfach ab ohne irgendeinen Nutzen außer ein bisschen Ego-Inflation. Wenn Sie nicht beabsichtigen, Ihre Anwendung auf MSDOS zu portieren, lassen Sie das Betriebssystem seine Aufgabe erfüllen. – Eclipse

+0

Das Warum ist, weil schließlich die Disziplin und die Funktionalität Ihnen helfen, Speicherlecks aufzuspüren. Wenn Sie es richtig codieren, kann es über eine DEFINE- oder MACRO-Definition ausgeschaltet werden. – ojblass

3

Sie müssen sich keine Sorgen machen, Speicher freizugeben, wenn exit() aufgerufen wird. Wenn der Prozess beendet wird, wird das Betriebssystem den gesamten zugehörigen Speicher freigeben.

+0

Dies war nicht immer wahr. Es ist eine gute sanitäre Praxis, Malloc'ed Strukturen vor dem Verlassen zu befreien. Es hilft auch, wenn Sie tatsächliche Speicherlecks finden müssen. –

+0

Dies ist nur für nette Betriebssysteme. Es ist gut,() Speicher freizugeben, wenn du damit fertig bist, nur um dich daran zu gewöhnen. –

+0

Richtig, ich nehme an, er diskutiert kritische Fehler; der Prozess stirbt und stirbt schnell. – Michael

1

Sie können einen einfachen Speichermanager für Malloc-Speicher erstellen, der zwischen Bereichen/Funktionen aufgeteilt ist.

Registrieren Sie es, wenn Sie es malloc, es abmelden, wenn Sie es freigeben. Verfügen Sie über eine Funktion, die allen registrierten Speicher freigibt, bevor Sie exit aufrufen.

Es fügt ein wenig Overhead hinzu, aber es hilft, Speicher zu verfolgen. Es kann Ihnen auch helfen, lästige Speicherlecks zu finden.

3

Ich denke, um diese Frage angemessen zu beantworten, müssten wir über die Architektur Ihres gesamten Programms (oder Systems, oder was auch immer der Fall sein könnte) wissen.

Die Antwort ist: es kommt darauf an. Es gibt eine Reihe von Strategien, die Sie verwenden können.

Wie bereits erwähnt, können Sie auf einem modernen Desktop- oder Serverbetriebssystem exit() verwenden und sich nicht um den Speicher kümmern, den Ihr Programm zugewiesen hat.

Diese Strategie ändert, zum Beispiel, wenn Sie auf einem Embedded-Betriebssystem entwickeln, wo exit() vielleicht nicht alles aufzuräumen. Normalerweise sehe ich, wenn einzelne Funktionen aufgrund eines Fehlers zurückkehren, sie stellen sicher, dass alles, was sie selbst zugewiesen haben, bereinigt wird. Sie würden keine exit() Anrufe nach dem Anruf, sagen wir, 10 Funktionen sehen. Jede Funktion würde wiederum einen Fehler anzeigen, wenn sie zurückkehrt, und jede Funktion würde nach sich selbst aufräumen. Die ursprüngliche Funktion main() (wenn Sie es nicht main() nennen möchten) würde den Fehler erkennen, alle Speicher löschen, die es zugewiesen hatte, und die entsprechenden Maßnahmen ergreifen.

Wenn Sie nur Bereiche-in-Bereiche haben, ist es kein Hexenwerk.Schwierig wird es, wenn Sie mehrere Ausführungsthreads und gemeinsame Datenstrukturen haben. Dann benötigen Sie möglicherweise einen Garbage Collector oder eine Möglichkeit, Referenzen zu zählen und den Speicher freizugeben, wenn der letzte Benutzer der Struktur damit fertig ist. Wenn Sie beispielsweise die Quelle des BSD-Netzwerkstacks betrachten, sehen Sie, dass in einigen Strukturen ein Wert refcnt (Referenzzähler) verwendet wird, der über einen längeren Zeitraum "am Leben" gehalten und unter verschiedenen geteilt werden muss Benutzer. (Dies ist im Grunde was Müllsammler auch tun.)

0

Sehr einfach, warum nicht eine Referenz gezählte Implementierung, so wenn Sie ein Objekt erstellen und übergeben Sie inkrementieren und dekrementieren die Referenz gezählte Zahl (daran erinnern, zu sein atomar, wenn Sie mehr als einen Thread haben).

Wenn ein Objekt nicht mehr verwendet wird (Nullverweise), können Sie es auf sichere Weise löschen oder automatisch im Referenzzähler verringern.

1

Michaels Ratschlag ist vernünftig - wenn Sie aussteigen, müssen Sie sich keine Gedanken darüber machen, wie Sie den Speicher freigeben können, da das System es sowieso zurückgewinnt.

Eine Ausnahme bilden gemeinsam genutzte Speichersegmente - zumindest unter System V Shared Memory. Diese Segmente können länger bestehen als das Programm, mit dem sie erstellt werden.

Eine Option, die bisher nicht erwähnt wurde, ist die Verwendung eines Arena-basierten Speicherzuordnungsschemas, das auf dem Standard malloc() aufbaut. Wenn die gesamte Anwendung eine einzelne Arena verwendet, kann Ihr Bereinigungscode diese Arena freigeben, und alles wird sofort freigegeben. (APR - Apache Portable Runtime - bietet eine Pool-Funktion, die meiner Meinung nach ähnlich ist; David Hansons "C Interfaces and Implementations" bietet ein arena-basiertes Speicherzuweisungssystem; ich habe eines geschrieben, das Sie verwenden könnten, wenn Sie das wollten.) Sie kann sich das als "Garbage Collection des armen Mannes" vorstellen.

Als eine allgemeine Speicherdisziplin, sollten Sie jedes Mal, wenn Sie Speicher dynamisch zuweisen, verstehen, welcher Code es freigeben wird und wann es freigegeben werden kann. Es gibt ein paar Standardmuster. Das einfachste ist "in dieser Funktion zugewiesen; freigegeben, bevor diese Funktion zurückkehrt". Dies hält den Speicher weitgehend unter Kontrolle (wenn Sie nicht zu viele Iterationen in der Schleife ausführen, die die Speicherzuweisung enthält) und sie so definiert, dass sie für die aktuelle Funktion und die von ihr aufgerufenen Funktionen verfügbar gemacht werden kann. Offensichtlich müssen Sie einigermaßen sicher sein, dass die Funktionen, die Sie aufrufen, die Zeiger auf die Daten nicht verwerfen ("cache") und versuchen, sie später wieder zu verwenden, nachdem Sie den Speicher freigegeben und wiederverwendet haben. Das nächste Standardmuster wird durch und fclose() beispielhaft dargestellt; Es gibt eine Funktion, die einem Speicher einen Zeiger zuweist, der vom aufrufenden Code verwendet werden kann und dann freigegeben wird, wenn das Programm damit fertig ist. Dies wird jedoch oft dem ersten Fall sehr ähnlich - es ist normalerweise eine gute Idee, fclose() in der Funktion zu nennen, die heißt.

Die meisten der verbleibenden "Muster" sind etwas ad hoc.

1

Die Leute haben bereits darauf hingewiesen, dass Sie sich wahrscheinlich keine Gedanken über die Speicherfreigabe machen müssen, wenn Sie Ihren Code im Fehlerfall nur beenden (oder abbrechen). Aber nur für den Fall, hier ist ein Muster, das ich entwickelt habe und viel nutze, um im Fehlerfall Ressourcen zu erstellen und abzubauen.Hinweis: Ich zeige hier ein Muster, um einen Punkt zu machen, nicht um echten Code zu schreiben!

int foo_create(foo_t *foo_out) { 
    int res; 
    foo_t foo; 
    bar_t bar; 
    baz_t baz; 
    res = bar_create(&bar); 
    if (res != 0) 
     goto fail_bar; 
    res = baz_create(&baz); 
    if (res != 0) 
     goto fail_baz; 
    foo = malloc(sizeof(foo_s)); 
    if (foo == NULL) 
     goto fail_alloc; 
    foo->bar = bar; 
    foo->baz = baz; 
    etc. etc. you get the idea 
    *foo_out = foo; 
    return 0; /* meaning OK */ 

    /* tear down stuff */ 
fail_alloc: 
    baz_destroy(baz); 
fail_baz: 
    bar_destroy(bar); 
fail_bar: 
    return res; /* propagate error code */ 
} 

Ich kann wetten, dass ich einige Kommentare bekommen werde, die sagen, "das ist schlecht, weil Sie goto verwenden". Aber dies ist eine disziplinierte und strukturierte Verwendung von goto, die den Code klarer, einfacher und einfacher zu verwalten macht, wenn er konsequent angewendet wird. Sie können keinen einfachen, dokumentierten Abbauweg durch den Code ohne ihn erreichen.

Wenn Sie dies in realen kommerziellen Code sehen möchten, werfen Sie einen Blick auf, sagen wir arena.c from the MPS (was zufällig ein Speicher-Management-System ist).

Es ist eine Art von armer-Mann-Versuch ... Fertig-Handler, und gibt Ihnen etwas ein bisschen wie Destruktoren.

Ich werde jetzt wie ein Greybeard klingen, aber in meinen vielen Jahren der Arbeit am C-Code anderer Leute ist das Fehlen klarer Fehlerpfade oft ein sehr ernstes Problem, besonders im Netzwerkcode und anderen unzuverlässigen Situationen. Die Einführung von ihnen hat mich gelegentlich zu einem beträchtlichen Beratungseinkommen geführt.

Es gibt viele andere Dinge zu Ihrer Frage zu sagen - ich werde es nur mit diesem Muster verlassen, falls das nützlich ist.

+0

Das ist schlecht, weil du goto benutzt. Sorry, konnte nicht widerstehen: D –

+0

Wenn Sie Ihre 'baz' und' bar' mit 'NULL' initialisiert und Ihre Zerstörungsfunktion so gemacht haben, dass sie einen' NULL'-Parameter anmutig behandeln (wie 'frei'), dann würden Sie in der Lage sein, nur ein "fail:" -Label zu verwenden, wodurch es ein wenig weniger überfüllt wird. –

+0

Danke für den Vorschlag! Es ist eine Kodierregel in meinen hochzuverlässigen Projekten, um zu vermeiden, dass NULL für so ziemlich alles verwendet wird. NULL zu haben führt zu allen Arten von Fehlern. Wir vermeiden besonders die Verwendung von NULL, um auf eine spezielle Aktion hinzuweisen, die Sie ergreifen sollten. Aber es gibt einen ganzen Aufsatz, den ich über NULL schreiben könnte: P – rptb1

Verwandte Themen