11 Dynamische Speicherverwaltung
In diesem Kapitel geht es um die dynamische Speicherverwaltung. Mit den hier beschriebenen Mechanismen können Sie Speicherblöcke zur Laufzeit anfordern und diese auch zur Laufzeit wieder freigeben.
Bisher kennen Sie nur die Möglichkeit, statischen Speicher in Ihren Programmen zu verwenden. In der Praxis kommt es allerdings häufig vor, dass zur Laufzeit des Programms gar nicht bekannt ist, wie viele Daten gespeichert werden sollen und wie viel Speicher somit benötigt wird. Stellen Sie sich hierzu vor, Sie wollen ein Online-Spiel programmieren, das auch Spielerdaten speichert, wissen aber nicht, wie viele Spieler daran teilnehmen werden. Natürlich können Sie immer ein Array mit besonders viel Speicher anlegen. Allerdings müssen Sie hierbei Folgendes beachten:
-
Auch ein Array mit extra viel Speicherplatz ist nicht unendlich, sondern auf eine statische Speichergröße beschränkt. Wird die maximale Größe erreicht, stehen Sie wieder vor dem Problem der Speicherknappheit. Zwar gibt es VLA-Arrays, aber diese müssen seit C11 nur noch optional unterstützt werden.
-
Verwenden Sie ein Array mit extra viel Speicherplatz, verschwenden Sie sehr viele Ressourcen, die vielleicht von anderen Programmen auf dem System dringender benötigt werden.
-
Die Gültigkeit des Speicherbereichs eines Arrays verfällt, sobald der Anweisungsblock verlassen wird. Ausnahmen stellen globale und als static deklarierte Arrays dar.
In vielen Fällen dürfte es daher sinnvoller sein, den Speicher erst zur Laufzeit des Programms dynamisch zu reservieren, sobald er benötigt wird, und ihn anschließend wieder freizugeben, wenn er nicht mehr benötigt wird. Realisiert wird die dynamische Speicherverwaltung mit Zeigern und einigen Funktionen der C-Standardbibliothek. Genau deshalb mussten wir auch erst die Zeiger behandeln, denn diese Kenntnisse werden Sie jetzt wieder brauchen.
Im Gegensatz zu einem statischen Speicherbereich wie beispielsweise einem Array bedeutet das, dass hierbei ein gewisser Mehraufwand betrieben werden muss. Die Freiheit in C, Speicher mithilfe von Zeigern zu reservieren und zu verwalten, birgt natürlich auch Gefahren. Sie könnten beispielsweise auf eine undefinierte Adresse im Speicher zugreifen. Sie müssen sich auch darum kümmern, Speicher, den Sie reserviert haben, wieder freizugeben. Tun Sie das nicht, entstehen sogenannte Speicherlecks (engl. memory leaks), die bei länger oder dauerhaft laufenden Programmen (beispielsweise Serveranwendungen) den Speicher »zumüllen« können. Irgendwann kommen Sie dann unter Umständen um einen Neustart des Programms nicht mehr herum oder – noch schlimmer – Sie provozieren kritische Sicherheitslücken.
Der dynamische Speicherbereich, in dem Sie zur Laufzeit des Programms etwas anfordern können, wird Heap (oder auch Freispeicher) genannt. Dieser Freispeicher ist unabhängig vom Stack und wird auch nicht direkt vom Prozessor verwaltet, sondern von C-Bibliotheken und dem Betriebssystem. Wenn Sie Speicher von diesem Heap anfordern, erhalten Sie immer einen zusammenhängenden Bereich und nie einzelne Fragmente, wie das auf dem Stack der Fall sein kann. Den Stack haben Sie in Abschnitt 7.6, »Exkurs: Funktionen bei der Ausführung«, bereits kennengelernt.
11.1 Neuen Speicher zur Laufzeit reservieren
Für die einfache Anforderung von Speicher zur Laufzeit finden Sie in der Header-Datei stdlib.h die Funktionen malloc() (von engl. memory allocation) und ebenfalls in stdlib.h calloc(). Sehen wir uns zuerst die Syntax von malloc() an:
#include <stdlib.h>
void* malloc(size_t size);
Die Funktion malloc() fordert nur so viel zusammenhängenden Speicher an, wie im Parameter size angegeben wurde. Der Parameter size verwendet Bytes als Einheit. Zurückgegeben wird ein typenloser void-Zeiger mit der Anfangsadresse des zugeteilten Speicherblocks. Konnte durch das Betriebssystem kein zusammenhängender Speicherblock für malloc() reserviert werden, wie er mit size angegeben wurde, liefert malloc() NULL zurück.
Der Inhalt von erfolgreich reservierten Speicherbereichen ist am Anfang undefiniert. Der C-Standard fordert von der Funktion malloc() außerdem nur, dass mindestens die Menge Speicher reserviert wird, die Sie anfordern. Durch das Alignment kann aber durchaus auch ein wenig mehr Speicher verbraucht werden, als Sie angeben. Wenn das Alignment z. B. fordert, dass die Speicheradressen aller Speicherobjekte durch 4 teilbar sind, und Sie 25 Bytes für einen String anfordern, dann verbraucht Ihr Programm tatsächlich 28 Bytes Heap-Speicher.
Bei anderen Variablentypen kann Ihnen das allerdings nicht passieren. Benötigen Sie beispielsweise Speicher für 100 Variablen vom Typ int, können Sie diesen Speicherblock folgendermaßen reservieren:
int *iptr;
iptr=malloc(100 * sizeof(int)); // Speicher für 100 int-Typen
Hier lassen Sie bei der Reservierung von Speicher den sizeof-Operator entscheiden, wie groß der zu reservierende Datentyp ist. Meistens ist auch gewährleistet, dass der int-Typ genau so groß ist wie die Wortbreite Ihres Prozessors und deshalb wirklich genau der Speicher verbraucht wird, den Sie erwarten.
Es ist natürlich immer möglich, direkt die Größe des zu reservierenden Speichers in Bytes anzugeben. Bezogen auf das eben gezeigte Beispiel könnten Sie ebenfalls Speicher für 100 int-Typen reservieren:
int* iptr;
iptr = malloc (400); // 400 Bytes Speicher reservieren
Diese Art der Speicherreservierung ist allerdings mit Vorsicht zu genießen. Sie setzen in diesem Beispiel voraus, dass int auf dem System, auf dem das Programm übersetzt wird, vier Bytes breit ist. Das mag vielleicht relativ häufig zutreffen. Was ist aber, wenn ein int auf einem anderen System unterschiedlich breit ist, z. B. 2 Bytes? Sicherer ist es daher, den sizeof-Operator zu verwenden.
Listing 11.1 zeigt, wie Sie einen beliebigen Speicherblock dynamisch zur Laufzeit reservieren können:
00 // Kapitel11/malloc_beispiel.c
01 #include <stdio.h>
02 #include <stdlib.h>
03 int* iArray( unsigned int n ) {
04 int* iptr = malloc( n *(sizeof(int) ) );
05 if( iptr != NULL ) {
06 for(unsigned int i=0; i < n; i++) {
07 iptr[i] = i*i; // Alternativ: *(iptr+i)=...
08 }
09 }
10 return iptr;
11 }
12 int main(void) {
13 unsigned int val=0;
14 printf("Wie viele Elemente benoetigen Sie: ");
15 if( scanf("%u", &val) != 1 ) {
16 printf("Fehler bei der Eingabe\n");
17 return EXIT_FAILURE;
18 }
19 int* arr = iArray( val );
20 if( arr == NULL ) {
21 printf("Fehler bei der Speicherreservierung!\n");
22 return EXIT_FAILURE;
23 }
24 printf("Ausgabe der Elemente\n");
25 for(unsigned i=0; i < val; i++ ) {
26 printf("arr[%u] = %u\n", i, arr[i]);
27 }
28 if(arr != NULL) {
29 free(arr); // Freigabe des Speichers
30 }
31 return EXIT_SUCCESS;
32 }
Listing 11.1 »malloc_beispiel.c« erzeugt zur Laufzeit ein dynamisches Array mit »malloc()« und füllt dieses anschließend mit Quadratzahlen.
In diesem Beispiel werden Sie gefragt, für wie viele int-Elemente Sie Speicherplatz reservieren wollen. In Zeile (19) rufen Sie dann die Funktion iArray() mit der Anzahl der gewünschten int-Elemente auf. Die Adresse des Rückgabewertes übergeben Sie dem Zeiger arr. In der Funktion iArray() reservieren Sie in Zeile (04) n Elemente vom Typ int. In Zeile (05) wird überprüft, ob die Reservierung nicht NULL ist und ob Sie erfolgreich Speicher vom System erhalten haben. Ist dies der Fall, übergeben Sie den einzelnen Elementen in der for-Schleife (Zeile (06) bis (08)) Quadratzahlen von 0 bis n-1.
Der Zugriff auf die einzelnen Elemente mit iptr[i] oder *(iptr+i) wurde bereits in Kapitel 10, »Zeiger (Pointer)«, beschrieben und sollte kein Problem mehr darstellen. Nebenbei erwähnt: Sie haben in diesem Listing auch gleichzeitig ein dynamisches Array erstellt und verwendet.
Am Ende der Funktion in Zeile (10) geben Sie die Anfangsadresse auf den reservierten Speicherblock an den Aufrufer der Zeile (19) zurück. An dieser Stelle werden Sie festgestellt haben, dass Sie reservierten Speicher problemlos vom Heap aus an Funktionen übergeben können.
In der main()-Funktion wird in Zeile (20) noch überprüft, ob der Rückgabewert NULL war. In diesem Fall wäre die Speicherreservierung in der Zeile (04) fehlgeschlagen. In der for-Schleife (Zeile (25) bis (27)) werden die einzelnen Elemente ausgegeben.
Auf die Freigabe des Speichers mit der Funktion free(), die in der Zeile (29) verwendet wurde, gehen wir noch in Abschnitt 11.3, »Speicherblöcke wieder freigeben«, gesondert ein. Auf die Überprüfung in der Zeile (28), ob arr ungleich NULL ist, um dann den Speicher freizugeben, könnten Sie auch verzichten, weil auch ein free(NULL) erlaubt ist.
Das Programm gibt bei der Ausführung Folgendes aus:
Wie viele int-Elemente benoetigen Sie: 9
Ausgabe der Elemente
arr[0] = 0
arr[1] = 1
arr[2] = 4
arr[3] = 9
arr[4] = 16
arr[5] = 25
arr[6] = 36
arr[7] = 49
arr[8] = 64
Benötigen Sie eine Funktion, die neben der Reservierung eines zusammenhängenden Speichers auch noch den zugeteilten Speicher automatisch mit 0 initialisiert, können Sie die Funktion calloc() verwenden. Die Syntax unterscheidet sich nur geringfügig von der von malloc():
#include <stdlib.h>
void* calloc( size_t count, size_t size );
Hiermit reservieren Sie count * size Bytes zusammenhängenden Speicher. Es wird ein typenloser Zeiger auf void mit der Anfangsadresse des zugeteilten Speicherblocks zurückgegeben. Jedes Byte des reservierten Speichers wird außerdem automatisch mit 0 initialisiert. Konnte kein zusammenhängender Speicher mit count * size Bytes reserviert werden, gibt diese Funktion NULL zurück.
[»] »malloc()« versus »calloc()«
Der Vorteil von calloc() gegenüber malloc() liegt darin, dass calloc() jedes Byte mit 0 initialisiert. Allerdings bedeutet das auch, dass calloc() mehr Zeit als malloc() beansprucht.
Das kurze Beispiel in Listing 11.2 zeigt, wie Sie calloc() in der Praxis verwenden können:
00 // Kapitel11/calloc_beispiel.c
01 #include <stdio.h>
02 #include <stdlib.h>
03 int main(void) {
04 unsigned int val=0;
05 printf("Wie viele int-Elemente benoetigen Sie: ");
06 if( scanf("%u", &val) != 1 ) {
07 printf("Fehler bei der Eingabe\n");
08 return EXIT_FAILURE;
09 }
10 int* arr = calloc( val, (sizeof(int) ) );
11 if( arr == NULL ) {
12 printf("Fehler bei der Speicherreservierung!\n");
13 return EXIT_FAILURE;
14 }
15 printf("Ausgabe der Elemente\n");
16 for(unsigned int i=0; i < val; i++ ) {
17 printf("arr[%u] = %u\n", i, arr[i]);
18 }
19 if( arr != NULL ) {
20 free(arr);
21 }
22 return EXIT_SUCCESS;
23 }
Listing 11.2 »calloc_beispiel.c« demonstriert die Verwendung von »calloc()«.
Listing 11.2 entspricht zum Teil dem Beispiel, das Sie in Listing 11.1 gesehen haben. Hier wurde aber in Zeile (10) der Speicher mit calloc() in der main()-Funktion reserviert und nicht mit einem Wert initialisiert. Dass calloc() alle Elemente mit 0 initialisiert, bestätigt die Ausgabe der for-Schleife in den Zeilen (16) bis (18).
[»] Die Funktion aligned_alloc()
Seit dem C11-Standard gibt es die Funktion align_alloc(), mit der Sie im Gegensatz zu malloc() neben der gewünschten Größe des Speicherblocks auch die Anordnung (engl. alignment) im Speicher explizit bestimmen können. Der Prototyp zu align_alloc() sieht wie folgt aus:
void* aligned_alloc(size_t alignment, size_t size);
Dies soll allerdings nur zu Ergänzung des Kapitels dienen. Auf das Anordnen von Speicherbereichen gehen wir in diesem Buch nicht ein.