12. Dynamische Speicherverwaltung

Bisher wurden nur Datentypen behandelt, deren Speicherplatzbedarf bereits zur Compilierungszeit feststanden und beim Erzeugen des Programms eingeplant werden konnten. Es ist jedoch nicht immer möglich, den Speicherbedarf exakt vorher zu planen, und es ist unökonomisch, jedesmal sicherheitshalber den maximalen Speicherplatz zu reservieren. C bietet daher die Möglichkeit, Speicherbereiche während des Programmlaufs zu reservieren, d.h. dem Programm zur Verfügung zu stellen. Wichtig ist, dass die reservierten Speicherbereiche spätestens zum Programmende wieder freigegeben werden.

12.1. Speicherbereiche reservieren

Speicherbereiche reservieren heißt, zur Laufzeit des Programms einen zusammenhängenden Speicherbereich dem Programm zugänglich zu machen und nach außen hin diesen Speicherbereich als belegt zu markieren. Dieser Speicherbereich liegt im sogenannten Heap (zu deutsch Halde), einem großen Speicherbereich, der vom Betriebssystem verwaltet wird.

Zum Reservieren von Speicherbereichen wird eine der beiden Funktionen malloc (steht für Memory Allocation) oder calloc (steht für Cleared Memory Allocation) verwendet. Dazu muss noch die Header-datei stdlib.h oder malloc.h eingebunden werden. Die Funktionen sind wie folgt deklariert:

void *malloc(int Groesse);
void *calloc(int Anzahl_Elemente, int Groesse_eines_Elements);


Beide Funktionen liefern einen void-Zeiger auf den reservierten Speicherbereich (bei calloc wird der Speicher mit 0 initialisiert, bei malloc sind die Werte des Speicherbereichs undefiniert) oder den NULL-Zeiger, wenn nicht mehr genügend Speicher vorhanden ist. Wird beim Reservieren des Speichers ein Zeiger auf einen anderen Datentypen benötigt, wird der Ergebniszeiger von malloc bzw. calloc entsprechend implizit umgewandelt (siehe auch Typumwandlung). Achtung: In C++ (d.h. auch bei Verwendung eines C++-Compilers für die Programmierung in C) muss er explizit umgewandelt werden!

Beispiel:

kap12_01.c

01 #include <stdio.h>
02 #include <stdlib.h>
03
04 int main()
05 {
06    double *t = malloc(2 * sizeof(*t));
07
08    if (t != NULL)
09    {
10       *t = 3.1415296;
11       *(t + 1) = 2 * *t;
12       printf("    PI = %f\n", *t);
13       printf("2 * PI = %f\n", *(t + 1));
14
15       free(t);   /* Speicher wieder freigeben */
16    }
17    else
18       printf("Kein Speicher verfuegbar!\n");
19
20    return 0;
21 }

Zum Zeitpunkt der Compilierung wird nur der Platz für den Zeiger t eingeplant. Mit der Initialisierung des Zeigers wird Speicherplatz in der Größe von 2 * sizeof(*t) Bytes zur Laufzeit des Programms bereitgestellt. Der Zeiger t zeigt anschließend auf diesen Speicherplatz (sofern Speicher vorhanden ist!).

Da Arrays und Zeiger intern identisch dargestellt und verarbeitet werden, lässt sich das oben angegebene Beispielprogramm auch mit Arrays schreiben.

Beispiel:

kap12_02.c

01 #include <stdio.h>
02 #include <stdlib.h>
03
04 int main()
05 {
06    double *t = malloc(2 * sizeof(*t));
07
08    if (t != NULL)
09    {
10       t[0] = 3.1415296;
11       t[1] = 2 * t[0];
12       printf("    PI = %f\n", t[0]);
13       printf("2 * PI = %f\n", t[1]);
14
15       free(t);   /* Speicher wieder freigeben */
16    }
17    else
18       printf("Kein Speicher verfuegbar!\n");
19
20    return 0;
21 }

Es lassen sich auch Speicherbereiche für Strukturen reservieren. Im folgenden Beispiel wird eine Struktur für imaginäre Zahlen definiert. Dann wird mit Hilfe der calloc-Funktion ein Speicherbereich für 2 Strukturen reserviert und dieser Bereich mit Werten gefüllt.

Beispiel:

kap12_03.c

01 #include <stdio.h>
02 #include <stdlib.h>
03
04 int main()
05 {
06    struct Imag
07    {
08       double Re;
09       double Im;
10    };
11    struct Imag *I = calloc(2, sizeof(*I));
12
13    if (I != NULL)
14    {
15       I->Re = 1;
16       I->Im = 0;
17       (I + 1)->Re = 0;
18       (I + 1)->Im = 1;
19       printf("  *I   = %f / %f\n", I->Re, I->Im);
20       printf("*(I+1) = %f / %f\n", (I + 1)->Re, (I + 1)->Im);
21
22       free(t);   /* Speicher wieder freigeben */
23    }
24    else
25       printf("Kein Speicher verfuegbar!\n");
26
27    return 0;
28 }

Die Größe des mit malloc bzw. calloc reservierten Speicherbereiches lässt sich mit der Funktion realloc vergrößern oder verkleinern. Dazu muss der Funktion ein Zeiger auf den bisherigen Speicherbereich und die neue Größe in Bytes angegeben werden. Je nach Compiler wird intern der reservierte Speicherbereich erweitert bzw. reduziert (sofern hinter dem bisher reservierten Speicherbereich noch genügend freier Heap vorhanden ist) oder es wird ein neuer Speicherbereich in der gewünschten Größe reserviert und der alte Speicherbereich anschließend freigegeben. Dabei bleiben die Daten vom alten Speicherbereich erhalten, d.h. ist der neue Speicherbereich größer, werden die Daten komplett in den neuen Speicherbereich kopiert, der restliche Speicherbereich wird nicht initialisiert. Ist dagegen der neue Speicherbereich kleiner, werden die Daten nur bis zur Größe des neuen Speicherbereichs kopiert, der Rest geht verloren.

Die Funktion ist folgendermaßen deklariert:

void *realloc(void *AlterSpeicherbereich, int NeueGroesse);

Beispiel:

kap12_04.c

01 #include <stdio.h>
02 #include <stdlib.h>
03
04 int main()
05 {
06    int *pArray = malloc(5 * sizeof(int));
07    int i;
08
09    if (pArray)
10    {
11       for (i = 0; i < 5; i++)
12       {
13          *(pArray + i) = i + 1;
14          printf("%i\n", *(pArray + i));
15       }
16
17       pArray = realloc(pArray, 1000 * sizeof(int));
18
19       if (pArray)
20       {
21          for (i = 0; i < 1000; i++)
22          {
23             *(pArray + i) = i + 1;
24             printf("%i\n", *(pArray + i));
25          }
26
27          free(pArray);
28       }
29    }
30
31    return 0;
32 }

Bei diesem Beispielprogramm wird erst Speicher für 5 Integerwerte reserviert, die anschließend in einer Schleife gesetzt und auf dem Bildschirm ausgegeben werden. Dann wird in Zeile 17 der reservierte Speicherbereich erweitert auf 1000 Integerwerte, die genauso in einer Schleife gesetzt und auf dem Bildschirm ausgegeben werden. Wird die Zeile 17 auskommentiert, kommt es zur Laufzeit zu einem Speicherzugriffsfehler.

Besonderheiten der realloc-Funktion: Wird als neue Größe eine 0 angegeben, wird der alte Speicherbereich freigegeben; die Funktion realloc entspricht dann der Funktion free (siehe nächster Abschnitt). Wird als alter Speicherbereich der NULL-Zeiger angegeben, wird nur Speicher in der angegebenen Größe reserviert, d.h. die realloc-Funkion entspricht der malloc-Funktion.

12.2. Reservierte Speicherbereiche freigeben

Die free-Funktion (benötigt ebenfalls die Headerdatei stdlib.h oder malloc.h) gibt den reservierten Speicherbereich wieder frei, damit dieser von neuem belegt oder anderen Programmen zur Verfügung gestellt werden kann. Dazu wird der Zeiger, der auf den freizugebenden Speicherbereich zeigt, der Funktion als Parameter angegeben. Im folgenden Beispiel wird ein Speicherbereich reserviert und gleich wieder freigegeben.

Beispiel:

kap12_05.c

01 #include <stdio.h>
02 #include <stdlib.h>
03
04 int main()
05 {
06    void *z = malloc(1000); /* 1000 Bytes Speicher reservieren */
07
08    if (z != NULL)
09    {
10       printf("Speicher reserviert!\n");
11       free(z);             /* Speicher wieder freigeben       */
12    }
13    else
14       printf("Kein Speicher verfuegbar!\n");
15
16    return 0;
17 }

Nach dem Freigeben eines reservierten Speicherbereichs kann auf diesen nicht mehr zugegriffen werden!

12.3. Hinweise für die Verwendung von malloc, calloc und free

Einige Dinge sollten bei der dynamischen Speicherverwaltung beachtet werden, deren Missachtung oder Unkenntnis in manchen Fällen ein unvorhersehbares Programmverhalten bzw. einen Systemabsturz nach sich zieht, leider ohne Fehlermeldung vom Compiler oder vom Betriebssystem.

• Die free-Funktion darf ausschließlich Speicherbereiche freigeben, die zuvor mit malloc oder calloc reserviert wurden!

   int i, *ip1,*ip2;
   ip1 = malloc(sizeof(*ip1));
   ip2 = &i;
   free(ip1); /* OK! */
   free(ip2); /* Fehler!!! */


• Die free-Funktion darf jeden reservierten Speicherbereich nur einmal freigeben. Falls zwei oder mehr Zeiger auf den gleichen reservierten Speicherbereich zeigen, darf free nur mit einen dieser Zeiger aufgerufen werden. Die anderen Zeiger verweisen danach wohl noch auf den ehemals reservierten Speicherbereich, dürfen aber nicht darauf zugreifen. Solche Zeiger werden dann hängende Zeiger (im englischen dangling pointer) genannt.

free(NULL); bewirkt nichts; verursacht auch keinen Fehler.

• Reservierte Speicherbereiche unterliegen nicht den Gültigkeitsregeln von Variablen. D.h. sie existieren unabhängig von Blockgrenzen solange, bis sie wieder freigegeben werden oder das Programm beendet wird.

• Reservierte Speicherbereiche, auf die kein Zeiger mehr zeigt, sind nicht mehr zugänglich und werden verwitwete Bereiche genannt. Die englische Bezeichnung trifft das daraus resultierende Problem besser: memory leak (Leck im Speicher). Werden regelmäßig neue Speicherbereiche reserviert, ohne sie wieder freizugeben, bricht das Programm irgendwann wegen Speicherknappheit ab. Daher sollte gut darauf geachtet werden, nicht mehr benötigte Speicherbereiche wieder freizugeben.

Ein weiterer Grund für das Abstürzen von Programmen, die über lange Zeit laufen, liegt in der Zerstückelung des Heap durch ständiges Reservieren und Freigeben von Speicherbereichen. Mit Zerstückelung ist gemeint, dass sich kleinere belegte und freie Bereiche abwechseln, so dass das Reservieren eines größeren zusammenhängenden Speicherbereichs nicht mehr erfüllt werden kann, obwohl die Summe aller einzelnen freien Plätze ausreichen würde.

Dieses Problem verlangt ein Zusammenschieben aller belegten Plätze, so dass ein großer freier Bereich entsteht. Dies wird garbage collection (zu deutsch Müllsammlung) genannt. Die meisten C/C++-Compiler haben in ihrer Speicherverwaltung aus Effizienzgründen keine garbage collection eingebaut, weil sie nur in wenigen Fällen nötig ist und viel Rechenzeit benötigt.