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 (also 2 * sizeof(double)
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, calloc
oder realloc 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 (Speicherleck). 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.
Voriges Kapitel: 11. Datei-Ein- und Ausgabe in C