11 Dynamische Speicherverwaltung
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 gäbe es hier VLA-Arrays, aber diese brauchen seit C11 nur noch optional unterstützt zu 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 wurde. 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 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 benötigen.
Im Gegensatz zu einem statischen Speicherbereich wie beispielsweise ein 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 eng. memory allocation) und ebenfalls in stdlib.h calloc(). Es folgt zuerst die Syntax von malloc():
#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.
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.
Es ist auch möglich, direkt die Größe des zu reservierenden Speichers in Bytes anzugeben. Bezogen auf das soeben 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.
Es folgt nun ein Beispiel, 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 }
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 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 Zeile (29) verwendet wurde, wird noch in Abschnitt 11.3, »Speicherblöcke wieder freigeben«, Speicherblöcke freigeben, gesondert eingegangen. Auf die Überprüfung in Zeile (28), ob arr ungleich NULL ist, um dann den Speicher freizugeben, könnten Sie auch verzichten, weil auch ein free(NULL) erlaubt ist. Aber mehr dazu in Kürze.
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():
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.
Hierzu ein kurzes Beispiel, 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 }
Das Listing entspricht zum Teil dem Beispiel malloc_beispiel.c zuvor. Hier wurde aber der Speicher mit calloc() in Zeile (10) 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 (alignment) im Speicher angeben können. Der Prototyp zu align_alloc() siehe 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 wird in diesem Buch nicht eingegangen.