[FAQ] Dynamischer Speicher mit malloc/calloc/realloc/free



  • Was haltet ihr von folgendem, geplanten FAQ-Eintrag zum Thema?

    <- snip ->

    0. Read the fine manual

    Als Begleitlektüre: man: malloc(3), man: calloc(3), man: realloc(3), man: free(3)

    1. malloc() nicht casten

    Man sieht es immer wieder, malloc() (bzw. der Rückgabewert) wird explizit in den Typen der empfangenden Variable gecastet. Das ist in C absolut unnötig, wird als schlechter Stil angesehen und kann zu Problemen führen.

    1.1 Warum ist es unnötig?

    Die Sprache C kennt seit ihrer ANSI-Standardisierung im Jahre 1989 den allseits bekannten void -Zeiger. Dieser hat die Eigenschaft, dass er implizit in Zeiger anderen Typs umgewandelt wird. Eine explizite Umwandlung ist damit unnötig.

    Ausnahmen:
    - der Code soll (aus welchen Gründen auch immer) kompatibel zu C++ sein.
    - vor der ANSI-Standardisierung gab es den void -Zeiger nicht, malloc() gab damals einen char * zurück, welcher dann gecastet werden musste. Dies sollte aber wirklich nur noch bei der Wartung von sehr altem Code ein Thema sein.

    1.2 Zu welchen Problemen kann es kommen?

    Sollte man vergessen haben, die Header-Datei stdlib.h einzubinden, könnte der Compiler annehmen, dass malloc() ein Integer zurückgibt, was natürlich falsch ist. Ohne Typecast wird der Compiler den Code aller Wahrscheinlichkeit nach mit einer Warnung versehen. Mit Typecast aber würde eben diese Warnung verloren gehen, und der Fehler des vergessenen Headers wäre hier nicht mehr (durch Warnungen) zu erkennen.

    Hinweis:
    - aktuelle Compiler würden auch mit Typecast eine Warnung ausgeben. z.B. der gcc: "warning: implicit declaration of function ‘malloc’"

    2. Hartkodierte Typen im Aufruf von malloc() vermeiden

    Betrachten wird einen typischen Aufruf von malloc() :

    short *ptr;
    
    // andere Deklarationen
    
    // anderer Code
    
    ptr = malloc ( sizeof(short) );
    
    free ( ptr );
    

    Zwar ist dieser Code nicht falsch, er enthält aber einen kleinen Fallstrick:
    Würde der Programmierer die Deklaration von ptr in Zeile 1 zu einem späteren Zeitpunkt von short zu int ändern, aber vergessen das sizeof beim Aufruf von malloc() in Zeile 7 auch zu ändern, dann würde (je nach System) u.U. zu wenig Speicher für ein int reserviert werden und der Code (mit etwas Glück) crashen.

    Dieses Problem kann vermieden werden, indem man den sizeof -Operator nicht auf einen Typ, sondern auf ein konkretes Objekt anwendet:

    short *ptr;
    
    // andere Deklarationen
    
    // anderer Code
    
    ptr = malloc ( sizeof(*ptr) );
    
    free ( ptr );
    

    Hier sollte klar sein, dass eine Änderung des Typs in Zeile 1 auch automatisch in Zeile 7 einfliesst. Wichtig ist hier zu erkennen, dass die Zeigervariable ptr bei sizeof immer dereferenziert werden muss.

    Ausnahme:
    - Soll Speicher für einen void-Zeiger reserviert werden, geht diese Variante nicht, da void * nicht dereferenziert werden kann.

    3. Zu jedem malloc() ein free()

    Da der Aufruf von malloc() (ebenso calloc() und evtl. auch realloc() [*]) Speicher reserviert, welcher auch über den Scope des Aufrufs erhalten bleibt, ist es notwendig diesen Speicher irgendwann (meistens gleich dann wenn er nicht mehr benötigt wird) wieder mit free() freizugeben. Wird dies nicht getan, entstehen Speicherlecks, welche je nach Programm unkritisch bis fatal sein können. In jedem Fall ist ein Speicherleck aber unschön und sollte vermieden werden.

    [*] Normalerweise wird mit realloc() ein bereits reservierter Speicherbereich vergrößert oder verkleinert. Ist allerdings das erste Argument von realloc() gleich NULL , dann ist der Aufruf äquivalent zu einem Aufruf von malloc() .

    3.1 Loggen von Reservierungen/Freigaben

    Eine einfache aber bereits wirksame Methode zum Überprüfen von Reservierungen/Freigaben ist es, die jeweiligen Funktionen zu wrappen und ihre Aufrufe einfach mitzuzählen. Die Implementierung könnte wie folgt aussehen:

    /* In der Datei wrapper.h würden natürlich die Prototypen der
    Funktionen stehen und stdlib.h sowie stdio.h eingebunden */
    #include "wrapper.h"
    
    /* Zaehlervariablen */
    static int num_alloc;
    static int num_free;
    
    void *wrapper_malloc ( size_t size ) {
    	void *tmp = malloc ( size );
    	if ( tmp != NULL ) {
    		num_alloc++;
    	}
    	return tmp;
    }
    
    void *wrapper_calloc ( size_t nmemb, size_t size ) {
    	void *tmp = calloc ( nmemb, size );
    	if ( tmp != NULL ) {
    		num_alloc++;
    	}
    	return tmp;
    }
    
    void *wrapper_realloc ( void *ptr, size_t size ) {
    	void *tmp = realloc ( ptr, size );
    	// wenn ptr gleich NULL, dann aequivalent zu malloc() behandeln
    	if ( tmp != NULL && ptr == NULL ) {
    		num_alloc++;
    	}
    	return tmp;
    }
    
    void  wrapper_free ( void *ptr ) {
    	free ( ptr );
    	num_free++;
    }
    
    void  wrapper_stats ( void ) {
    	printf ( "Number of   allocations   : %10d\n", num_alloc );
    	printf ( "Number of deallocations   : %10d\n", num_free  );
    	printf ( "Therefore lost allocations: %10d\n", num_alloc - num_free );
    }
    

    3.2 realloc() richtig angewendet

    Bei der Verwendung von realloc() zum vergrößern/verkleinern eines bereits reservierten Speicherbereiches übergibt man einen Zeiger auf eben diesen Speicherbereich.

    // für ptr wurde bereits Speicher reserviert
    ptr = realloc ( ptr, 10 * sizeof(*ptr) );
    

    Das Problem bei diesem Code: Es könnte (warum auch immer) passieren, dass eine Vergößerung nicht möglich ist. In diesem Fall würde realloc() den Wert NULL zurückliefern. Dabei würde die in ptr abgelegte Adresse allerdings verlorengehen und damit auch die wahrscheinlich einzige Möglichkeit auf den Speicher dahinter zuzugreifen. Besser ist es, ptr erst dann die neue Adresse zuzuweisen, nachdem man das Ergebnis von realloc() getestet hat:

    // für ptr wurde bereits Speicher reserviert
    void *temp;
    temp = realloc ( ptr, 10 * sizeof(*ptr) );
    if ( temp != NULL ) {
        ptr = temp;
    }
    

    3.3 Tools zum prüfen auf Speicherlecks

    Eine (noch) kleine Sammlung an Tools/Libs zum aufspüren von Speicherlecks:
    - Valgrind
    - DUMA

    <- snap ->

    Bei den TODO: Tags könntet ihr mir helfen, ansonsten natürlich Verbesserungsvorschläge posten etc. Aber das Ding soll kein Roman werden 😉

    Gruß und guten Rutsch



  • Tim schrieb:

    Tags könntet ihr mir helfen, ansonsten natürlich Verbesserungsvorschläge posten etc. Aber das Ding soll kein Roman werden 😉

    ist eh' schon lang, ein paar links hätten auch gereicht, oder du machst 'nen beitrag für's cpp.de-magazin daraus.
    ansonsten: der quelltext von einer echten malloc/free/realloc-implemantation wär' vielleicht nett, damit jeder sehen kann wie sowas geht. und vielleicht könntest du noch alternativen nennen für dynamisches speichermanagement.
    🙂



  • rezensions-freak schrieb:

    Tim schrieb:

    Tags könntet ihr mir helfen, ansonsten natürlich Verbesserungsvorschläge posten etc. Aber das Ding soll kein Roman werden 😉

    ist eh' schon lang, ein paar links hätten auch gereicht, oder du machst 'nen beitrag für's cpp.de-magazin daraus.

    Nee, das will ich eigentlich nicht.

    rezensions-freak schrieb:

    ansonsten: der quelltext von einer echten malloc/free/realloc-implemantation wär' vielleicht nett, damit jeder sehen kann wie sowas geht. und vielleicht könntest du noch alternativen nennen für dynamisches speichermanagement.
    🙂

    Ersteres wäre imho nicht passend für einen FAQ-Eintrag bez. malloc und Konnsorten. Mit "Alternativen" meinst sowas wie böhm-gc? Könnte man erwähnen, ja.



  • Tim schrieb:

    Mit "Alternativen" meinst sowas wie böhm-gc? Könnte man erwähnen, ja.

    ne, eher sowas wie 'Simple Segregated Storage' --> http://www.boost.org/libs/pool/doc/concepts.html
    🙂



  • O.K. aber das würde imho zu weit führen.



  • Tim schrieb:

    O.K. aber das würde imho zu weit führen.

    stimmt, dein text behandelt ja speziell malloc/realloc/free usw.
    na gut, was mir noch einfällt:
    - was passiert wenn man malloc(0), free(0) usw. aufruft?
    - wie reagieren wenn malloc 0 zurückgibt?
    - probleme mit malloc/realloc und IPC, shared libraries, dlls (verschiedene heaps!)?
    - werdem memory leaks hinterlassen, wenn ein programm abschmiert.
    - vorteile/nachteil von malloc/free/realloc in systemen mit wenig speicher.
    - wo bekommen malloc/realloc den speicher her?
    🙂



  • heap-fan schrieb:

    Tim schrieb:

    O.K. aber das würde imho zu weit führen.

    stimmt, dein text behandelt ja speziell malloc/realloc/free usw.
    na gut, was mir noch einfällt:
    - was passiert wenn man malloc(0), free(0) usw. aufruft?
    - wie reagieren wenn malloc 0 zurückgibt?
    - probleme mit malloc/realloc und IPC, shared libraries, dlls (verschiedene heaps!)?
    - werdem memory leaks hinterlassen, wenn ein programm abschmiert.
    - vorteile/nachteil von malloc/free/realloc in systemen mit wenig speicher.
    - wo bekommen malloc/realloc den speicher her?
    🙂

    gut, dann fang schonmal an 🙂



  • heap-fan schrieb:

    Tim schrieb:

    O.K. aber das würde imho zu weit führen.

    stimmt, dein text behandelt ja speziell malloc/realloc/free usw.
    na gut, was mir noch einfällt:
    - was passiert wenn man malloc(0), free(0) usw. aufruft?
    - wie reagieren wenn malloc 0 zurückgibt?
    - probleme mit malloc/realloc und IPC, shared libraries, dlls (verschiedene heaps!)?
    - werdem memory leaks hinterlassen, wenn ein programm abschmiert.
    - vorteile/nachteil von malloc/free/realloc in systemen mit wenig speicher.
    - wo bekommen malloc/realloc den speicher her?
    🙂

    Sorry, aber das ist mir schon zu speziell (Plattformspezifisch etc.) als dass ich das in einen FAQ-Eintrag hier in ANSI C behandeln würde. Das wären imho alles Themen für einen Artikel.

    Und bez. "wie reagieren wenn malloc 0 zurückgibt?": Da gibts doch eh kein 100%-Gelingen-Rezept. Da gabs in "Rund um die Programmierung" erst letztens eine Diskussion, ob man dem Fall überhaupt noch die Chance hat was zu retten etc. Nein, das würde den Eintrag sprengen. Ich will hier kein "C von A bis F" schreiben.



  • jepp schrieb:

    gut, dann fang schonmal an

    okay, -->
    X. malloc(0)
    Ein Aufruf von malloc() mit 0 als Argument ist implementationsabhängig. Es sind zwei Resultate möglich.
    1. malloc() gibt einen NULL Zeiger zurück.
    2. malloc() gibt einen Zeiger ungleich NULL zurück. Obwohl kein benutzbarer Speicher alloziert wurde, existiert auf dem Heap ein Listeneintrag, der mit free() wieder gelöscht werden muß.



  • 👍 gut,
    man könnte sich die frage stellen, welcher grund einen programmierer bewegen könnte 0 byte arbeitsspeicher anzufordern *grübel*



  • jepp schrieb:

    man könnte sich die frage stellen, welcher grund einen programmierer bewegen könnte 0 byte arbeitsspeicher anzufordern *grübel*

    das könnte z.b. bei berechneten mallocs passieren, etwa bei malloc (a * sizeof(b));, wenn b 0 ist.
    🙂



  • Gut, aber das ist dann ein Bug den man fixt ohne wissen zu müssen wie malloc() in so einem Fall reagiert (was ja eh wieder "implementation defined" wäre).

    edit: außerdem meinst du wohl eher "wenn a 0 ist" 😉



  • Tim schrieb:

    edit: außerdem meinst du wohl eher "wenn a 0 ist"

    ja, das meinte ich.
    🙂



  • Ich fände es gut, wenn du die richtige Verwendung von man: realloc(3) erklären könntest. Man sieht zu oft p = realloc(p, N) .



  • rüdiger schrieb:

    Ich fände es gut, wenn du die richtige Verwendung von man: realloc(3) erklären könntest. Man sieht zu oft p = realloc(p, N) .

    Das habe ich mir hier, etwas kryptisch vielleicht, notiert:

    TODO: Mögliches Speicherleck bei falschem Aufruf von realloc()



  • btw. zum casten von malloc. Auf m68k kann das wohl böse enden, da Adressen und Integer dort unterschiedliche Register haben: http://www.grep.be/blog/en/computer/debian/m68k/broken_c_code



  • rüdiger schrieb:

    btw. zum casten von malloc. Auf m68k kann das wohl böse enden, da Adressen und Integer dort unterschiedliche Register haben

    das war wohl eher ein unfähiger compiler, der casts nicht richtig beherrscht.
    🙂



  • nö, weil in dem Beispiel in der Datei mit der main-Funktion die Funktion als void *f(); deklariert wurde. Aber das ist auch total OT...



  • rüdiger schrieb:

    nö, weil in dem Beispiel in der Datei mit der main-Funktion die Funktion als void *f(); deklariert wurde.

    stimmt, du hast recht. falscher prototyp, das hab' ich übersehen.
    🙂


Anmelden zum Antworten