Bisher wurde die Speicherverwaltung in Variablendefinitionen versteckt. Man musste sich so zwar keine Gedanken bezüglich der Verwaltung machen, aber spätestens wenn neuer Speicher für weitere Daten benötigt wurde, musste der Code umgeschrieben werden. C C++ C/C++ dynamisch dynamische Speicherverwaltung Speicher allokieren allozieren malloc calloc realloc alloca Zeiger Arrays Dynamische Speicherverwaltung - Speicher allokieren allozieren malloc calloc realloc alloca Kapitel 17: Dynamische Speicherverwaltung

17.5. Speicher dynamisch reservieren mit realloc und calloc            zurück  Ein Kapitel tiefer  zum Inhaltsverzeichnis

In der Headerdatei <stdlib.h> sind noch zwei weitere Funktionen zum dynamischen Reservieren von Speicher deklariert. Hier die Syntax zu diesen Funktionen:

void *calloc(size_t anzahl, size_t groesse);
void *realloc(void *zgr, size_t neuegroesse);

Die Funktion calloc() ist der Funktion malloc() sehr ähnlich. Nur, dass es bei der Funktion calloc() nicht einen, sondern zwei Parameter gibt. Im Gegensatz zu malloc() können Sie mit calloc() noch die anzahl von Speicherobjekten angeben, die reserviert werden soll. Wird z.B. für 100 Objekte vom Typ int Speicherplatz benötigt, so erledigen Sie dies mit calloc() folgendermaßen:

int *zahlen;

zahlen=(int *)calloc(100,sizeof(int));

Außerdem werden mit der Funktion calloc() alle Werte des allozierten Speicherbereichs automatisch mit dem Wert 0 initialisiert. Bei malloc() hat der reservierte Speicherplatz zu Beginn einen undefinierten Wert.

Da calloc() außer den beiden eben genannten Unterschieden genauso funktioniert wie die Funktion malloc(), soll nicht mehr näher auf diese Funktion eingegangen werden.

Interessanter ist dagegen die dynamische Speicherreservierung mit der Funktion realloc(). Mit dieser Funktion ist es möglich, während des laufenden Programms so viel Speicher zu reservieren, wie Sie benötigen.

Mit realloc() ist es noch einfacher, z.B. dynamische Arrays zu programmieren. Die Anfangsadresse des dynamischen Arrays ist diejenige, auf die der Zeiger (zgr) zeigt. Der Parameter neuegroesse dient dazu, einen bereits zuvor allozierten Speicherplatz auf neuegroesse Bytes zu vergrößern. Die Funktion realloc() ermöglicht es auch, den Speicherplatz zu verkleinern. Dazu wird einfach der hintere Teil des Speicherblocks freigegeben, während der vordere Teil unverändert bleibt. Bei einer Vergrößerung des Speicherplatzes mit realloc() behält der vordere Teil auf jeden Fall seinen Wert, und der neue Teil wird einfach hinten angehängt. Dieser angehängte Wert ist aber wie bei malloc() undefiniert. Hier ein kleines Beispiel, wie ein Array mit der Funktion realloc() dynamisch erstellt wird:

#include <stdio.h>
#include <stdlib.h>

int main()
{
   int n=0,
       max=10,
       z,i,
       *zahlen=NULL;

/*Wir reservieren Speicher für 10 int-Werte mit calloc*/
   zahlen = (int *)calloc(max, sizeof(int));
   if(NULL == zahlen)
     {
       printf(".....Speicherplatzmangel!!!!!\n");
       exit(0);
     }
   printf("Zahlen eingeben --- Beenden mit 0\n");
   /* Endlossschleife */
   while(1)
     {
       printf("Zahl (%d) eingeben : ",n+1);
       scanf("%d",&z);
       if(z==0)
            break;
      /*Reservierung von Speicher während der Laufzeit
        des Programms mit realloc*/
       if(n>=max)
          {
             max+=max;
             zahlen = (int *)realloc(zahlen,max*sizeof(int));
             if(NULL == zahlen)
            {
              printf("Speicherplatzmangel!!!\n");
              exit(1);
            }
            printf("Speicherplatz reserviert "
                 " (%d Bytes)\n",sizeof(int)*max);
          }
       zahlen[n++]=z;
     }
   printf("Folgende Zahlen wurden eingegeben ->\n\n");
   for(i=0;i<n;i++)
        printf("%d ",zahlen[i]);
   printf("\n");
   free(zahlen);
   return 0;
}

Den benötigten Speicherbedarf könnten Sie in diesem Beispiel auch einzeln allozieren. Die einfache Anwendung dieser Funktion soll nicht darüber hinwegtäuschen, dass auch hier erst der alte Speicherbereich temporär zwischengespeichert werden muss, so wie bei der Funktion malloc(). In diesem Fall ist es aber einfacher, da Sie sich nicht mehr selbst darum kümmern müssen.

Im Beispiel wurde der Speicherplatz nach jedem erneuten Allozieren mit calloc() gleich verdoppelt (max+=max). Dies ist nicht optimal. Benötigt ein Programm z.B. täglich 500 double-Werte, wäre es am sinnvollsten, erst nach 500 double-Werten neuen Speicher zu allozieren. Somit müsste das Programm nur einmal am Tag neuen Speicher bereitstellen. Dasselbe Beispiel lässt sich recht ähnlich und einfach auch auf char-Arrays umschreiben. Das folgende Listing demonstriert die dynamische Erweiterung eines Strings:

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define BUF 255

int main()
{
   size_t len;
   char *str=NULL;
   char puffer[BUF];

   printf("Ein dynamisches char-Array für Strings\n");
   printf("Eingabe machen : ");
   fgets(puffer, BUF, stdin);
   str = (char *)malloc(strlen(puffer)+1);
   if(NULL == str)
      {
         printf("Konnte keinen Speicher bereitstellen...\n");
         exit(0);
      }
   strcpy(str, puffer);
   printf("Weitere Eingabe oder beenden mit \"END\"\n>");
   /* Endlossschleife */
   while(1)
     {
       fgets(puffer, BUF, stdin);
       /* Abbruchbedingung */
       if(strcmp(puffer,"end\n")==0||strcmp(puffer,"END\n")==0)
          break;
       /* Aktuelle Länge von str zählen für realloc */
       len = strlen(str);
       /* Neuen Speicher für str anfordern */
       str = (char *)realloc(str,strlen(puffer)+len+1);
       if(NULL == str)
          {
             printf("Konnte keinen Speicher bereitstellen...\n");
             exit(0);
          }
       /* Hinten Anhängen */
       strcat(str, puffer);
     }
   printf("Ihre Eingabe lautete: \n");
   printf("%s",str);
   free(str);
   return 0;
}

Beim char-Array läuft es ähnlich ab wie schon im Beispiel mit den int-Werten zuvor. Es muss allerdings immer darauf geachtet werden, dass bei erneuter Speicheranforderung mit realloc() das Stringende-Zeichen berücksichtigt wird (+1). Ansonsten ist der Vorgang recht simpel: String einlesen, Zeichen zählen, erneut Speicher reservieren und hinten anhängen.

17.6. Speicher vom Stack anfordern mit alloca (nicht ANSI C)            zurück  Ein Kapitel tiefer  Ein Kapitel höher  zum Inhaltsverzeichnis

Die Funktion alloca() ist nicht vom ANSI C-Standard vorgeschrieben. Da diese Funktion bei vielen Compilern vorhanden ist, soll sie hier kurz erwähnt werden. Die Syntax zu alloca() lautet:

void *alloca(size_t size);

Bei Linux/UNIX befindet sich alloca() in der Headerdatei <stdlib.h>, und unter MS-DOS/Windows sollte sich diese Funktion in der Headerdatei <malloc.h> befinden.

alloca() kann bezüglich der Verwendung mit malloc() verglichen werden, aber mit dem Unterschied, dass alloca() den Speicherplatz nicht vom Heap, sondern vom Stack anfordert.

alloca() hat außerdem den Vorteil, dass der Speicherplatz nicht extra mit free() freigegeben werden muss, da dieser automatisch beim Verlassen der Funktion freigegeben wird.

Die Funktion alloca() vergrößert den Stack-Bereich (Stack Frame) der aktuellen Funktion. Das ist auch der Grund, weshalb alloca() nicht überall verfügbar ist. Es ist auf manchen Systemen nicht möglich, den Stack-Bereich zu vergrößern. Die Funktion alloca() wird genauso verwendet wie die Funktion malloc().

17.7. free - Speicher wieder freigeben            zurück  Ein Kapitel tiefer  Ein Kapitel höher  zum Inhaltsverzeichnis

Die Syntax der Funktion zur Freigabe von Speicher lautet: #include void free(void *zeiger); free() wurde bereits des Öfteren verwendet. Diese Funktion dient der Freigabe von Speicher, der zuvor mit Funktionen wie malloc(), calloc() oder realloc() angefordert wurde. Folgendes sollten Sie bei dieser Funktion aber noch beachten:

Beim Allozieren des Speichers mit malloc() wird der Aspekt, den Speicher wieder freizugeben, häufig vernachlässigt. In den Beispielen dieses Buchs dürfte ein vergessenes free() nicht allzu tragisch sein, da ein Programm, das sich beendet, seinen Speicherplatz automatisch wieder freigibt. Schlimmer dürfte der Fall aber bei so genannten Server-Programmen sein, die oft wochen- bzw. jahrelang laufen müssen. Das Programm wird zwangsweise immer langsamer. Man spricht dabei von Memory Leaks (Speicherlecks). Das passiert sicherlich nur Anfängern? Das ist leider ganz und gar nicht so. Nicht umsonst verdienen sich viele Softwarehersteller mit Programmen, die solche und andere Programmierfehler entdecken, eine goldene Nase. Memory Leaks gehören neben Buffer Overflows zu den am häufigsten gemachten Fehlern von C-Programmierern. Mehr zu Memory Leaks finden Sie in Kapitel 26, Sicheres Programmieren.

17.8. Zweidimensionale dynamische Arrays            zurück  Ein Kapitel tiefer  Ein Kapitel höher  zum Inhaltsverzeichnis

In Abschnitt 15.9 haben Sie gelesen, dass das Anwendungsgebiet von Zeigern auf Zeiger unter anderem das dynamische Erstellen von Matrizen ist. Ich will Ihnen jetzt aber nicht die Suppe versalzen und als Thema die Matrizenberechnung nehmen, sondern ich werde nur einfache Speicherreservierungen mit Zeilen und Spalten vornehmen:

int matrix[zeile][spalte];

Um also für ein zweidimensionales Array mit beliebig vielen Zeilen und Spalten Speicher zu reservieren, benötigen Sie zuerst Platz für die Zeile. Und für jede dieser Zeilen wird nochmals Platz für die Spalte benötigt. Beim Freigeben des Speichers muss dies in umgekehrter Reihenfolge vorgenommen werden.

Hierzu das vollständige Listing:

#include <stdio.h>
#include <stdlib.h>
#define BUF 255

int main()
{
   int i, j, zeile, spalte;
   /* Matrix ist Zeiger auf int-Zeiger */
   int ** matrix;

   printf("Wie viele Zeilen : ");
   scanf("%d",&zeile);
   printf("Wie viele Spalten: ");
   scanf("%d",&spalte);

   /* Speicher reservieren für die int-Zeiger (=zeile) */
   matrix = (int **)malloc(zeile*sizeof(int *));
   if(NULL == matrix)
      {
         printf("Kein Speicher fuer die Zeilen...\n");
         exit(0);
      }
   /* Jetzt noch Speicher reservieren für die einzelnen Spalten
      der i-ten Zeile
    */
   for(i=0; i < zeile; i++)
      {
         matrix[i] = (int *)malloc(spalte*sizeof(int));
         if(NULL == matrix[i])
            {
               printf("Kein Speicher fuer Zeile %d\n",i);
               exit(0);
            }
      }
   /* Mit beliebigen Werten initialisieren */
   for (i = 0; i < zeile; i++)
       for (j = 0; j < spalte; j++)
         matrix[i][j] = i+j; /* matrix[zeile][spalte] */
   /* Inhalt der Matrix entsprechend ausgeben */
   for (i = 0; i < zeile; i++)
      {
          for (j = 0; j < spalte; j++)
              printf("%d ",matrix[i][j]);
          printf("\n");
      }

   /* Speicherplatz wieder freigeben
    * Wichtig! In umgekehrter Reihenfolge
    */
   /* Spalten der i-ten Zeile freigeben */
   for(i=0; i< zeile; i++)
      free(matrix[i]);
   /* Jetzt können die leeren Zeilen freigegeben werden */
   free(matrix);
  return 0;
}

Zugegeben, das Listing hat es in sich. Für einige dürfte es etwas undurchsichtig erscheinen, wie aus **matrix nun matrix[zeile][spalte] wird. Am besten sehen Sie sich einfach einmal an, was bei folgender Speicherreservierung geschehen ist:

matrix = (int **)malloc(zeile*sizeof(int *)); 

Als Beispiel soll eine Matrix 4 x 3 erstellt werden, also vier Zeilen und drei Spalten.

Abbildung 17.9: Reservierung des Speichers für die Zeile (erste Dimension)
Abbildung 17.9: Reservierung des Speichers für die Zeile (erste Dimension)

Nachdem Sie den Speicher für die einzelnen Zeilen reserviert haben, können Sie als Nächstes Speicher für die einzelnen Spalten reservieren.

for(i=0; i < zeile; i++)
   {
      matrix[i] = (int *)malloc(spalte*sizeof(int));
      if(NULL == matrix[i])
         {
            printf("Kein Speicher fuer Zeile %d\n",i);
            exit(0);
         }
   }

Somit ergibt sich im Speicher dann folgendes finale Bild:

Abbildung 17.10: Nach der Reservierung des Speichers für die Spalte
Abbildung 17.10: Nach der Reservierung des Speichers für die Spalte

Sicherlich erinnern Sie sich noch an die Demonstration des gleichwertigen Zugriffs auf ein Speicherobjekt mithilfe eines Zeigers und eines Arrays in Kapitel 15, Zeiger. Auch bei den Zeigern auf Zeiger und den zweidimensionalen Arrays gibt es einige äquivalente Fälle. Hierzu eine Tabelle:
Zugriff auf … Möglichkeit 1 Möglichkeit 2 Möglichkeit 3
1.Zeile, 1.Spalte **matrix *matrix[0] matrix[0][0]
i.Zeile, 1.Spalte **(matrix+i) *matrix[i] matrix[i][0]
1.Zeile, i.Spalte *(*matrix+i) *(matrix[0]+i) matrix[0][i]
i.Zeile, j.Spalte *(*(matrix+i)+j) *(matrix[i]+j) matrix[i][j]

Tabelle 17.2: Äquivalenz zwischen Zeigern auf Zeiger und mehrdim. Arrays

17.9. Wenn die Speicheralloktion fehlschlägt            zurück  Ein Kapitel höher  zum Inhaltsverzeichnis

In den vergangenen Abschnitten wurde die Speicherallozierung folgendermaßen verwendet:

#include <stdio.h>
#include <stdlib.h>

int main()
{
   int *ptr;
   ptr =(int *) malloc(100);

   if(NULL == ptr)
      {
         printf("Konnte keinen Speicher allozieren!\n");
         exit(0);
      }
   return 0;
}

Auf den ersten Blick scheint dieser Code auch in Ordnung. Es wird überprüft, ob die Funktion malloc() erfolgreich Speicher allozieren konnte. Stellen Sie sich jetzt vor, Sie arbeiten an einem Textverarbeitungsprogramm und haben ein paar Seiten Text zusammengestellt, den es jetzt abzuspeichern gilt. Das Programm alloziert noch Speicherplatz für den gesamten Text, bevor dieser in eine Datei abgespeichert werden kann. Jetzt, in diesem Moment, schreibt das Programm die Fehlerausgabe auf den Bildschirm und beendet sich. Der Text ist futsch und die Nerven des Anwenders auch. Es ist also kein guter Stil, ein Programm einfach zu beenden, wenn die Speicherallozierung fehlschlägt. Hierzu folgen jetzt einige theoretische Tipps, was Sie tun können, wenn eine Speicheranforderung nicht erfüllt werden konnte.

17.9.1 Speicheranforderung reduzieren
Kann partout kein Speicherblock einer bestimmten Größe angefordert werden, sollten Sie die Speicheranforderung ein wenig reduzieren. Vielleicht kann das System einfach keinen großen zusammenhängenden Block finden. Wie Sie die erneute Speicheranforderung reduzieren, bleibt Ihnen selbst überlassen. Eine Möglichkeit wäre es, den angeforderten Speicher durch zwei zu teilen. Ein Beispiel dazu:

#include <stdio.h>
#include <stdlib.h>
#define MIN_LEN 256

int main()
{
   int *ptr;
   char jn;
   static size_t len = 8192; /* Speicheranforderung */

   do{
     ptr =(int *) malloc(len);
     if(ptr == NULL) /* Speicher konnte nicht alloziert werden */
        {
           len/=2;  /* Versuchen wir es mit der Hälfte */
           ptr =(int *) malloc(len);
           if(ptr == NULL)
              {
                 printf("Konnte keinen Speicher allozieren. "\
                        " Weiter versuchen? (j/n): ");
                 scanf("%c",&jn);
                 fflush(stdin);
              }
        }
     else
        break; /* Erfolg. Speicherreservierung - Schleifenende */
   }while(jn != 'n' && len > MIN_LEN);
   /* So lange weiterprobieren, bis 'n' gedrückt wurde oder
      len weniger als MIN_LEN beträgt */
   if(len <= MIN_LEN)
      printf("Speicherandforderung abgebrochen!!\n");
   return 0;
}

Gelingt die Speicheranforderung hierbei nicht, wird der angeforderte Speicher um die Hälfte reduziert. Bei erneutem Versuch und eventuellem Scheitern wird der angeforderte Speicher wieder um die Hälfte reduziert. Dies läuft so lange fort, bis MIN_LEN Speicherplatzanforderung unterschritten wird oder der Anwender zuvor mit dem Buchstaben 'n' abbrechen will. Dies ist natürlich nur eine von vielen Strategien, die Sie anwenden können.

17.9.2 Speicheranforderungen aufteilen
So einfach wie im Beispiel eben werden Sie es aber höchstwahrscheinlich nicht haben. Was ist, wenn die Länge eines Strings oder die Größe einer Struktur bereits feststeht? Sie können nicht für einen String der Länge n einfach n/2 Bytes Speicherplatz anfordern. Schließlich soll ja nicht nur der halbe Text gespeichert werden. Wenn es Ihnen dabei nicht allzu sehr auf die Geschwindigkeit ankommt, könnten Sie die Funktion realloc() verwenden:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define BUF 8192

int main()
{
   char *buffer;
   int reserviert=0;
   int i;
   static size_t len = BUF; /* Speicheranforderung */

   buffer = (char *)malloc(sizeof("Hallo Welt"));
   strcpy(buffer, "Hallo Welt");

   while(reserviert != BUF && len != 0)
      {
         buffer =(char *) realloc(buffer, len);
         /* Speicher konnte nicht alloziert werden */
         if(buffer == NULL)
            {
               len/=2;  /* Versuchen wir es mit der Hälfte */
            }
         else
            {
               reserviert+=len;
            }
      }

   for(i=0; i<reserviert; i++)
      buffer[i] = 'x';
   buffer[i]='\0';
   printf("\n%s\n",buffer);
   return 0;
}

Dieses Listing erweist sich als ein hartnäckiger Fall der Speicherallozierung. Im String buffer soll zusätzlicher Speicherplatz von 8192 Bytes reserviert werden. Gelingt dies nicht, teilt das Programm diesen Happen in zwei Teile auf und versucht es erneut. Diese Aufteilung geht soweit, dass eventuell byteweise Speicherplatz reserviert wird. Damit Sie auch eine Abbruchbedingung im Programm haben, wird die Anzahl des erfolgreich reservierten Speichers mitgezählt. Die Funktion realloc() wird dazu verwendet, dass der neu allozierte Speicher jeweils hinten angefügt wird.

17.9.3 Einen Puffer konstanter Größe verwenden
Das Problem hat vielleicht nichts mit der dynamischen Speicherallozierung zu tun, aber manches Mal ist die dynamische Speicherreservierung fehl am Platz. Überdenken Sie das Programm dahin gehend, ob es nicht sinnvoller wäre, ein char-Array konstanter Größe zu verwenden. Ein einfaches Beispiel ist das Kopieren zweier Dateien.

17.9.4 Zwischenspeichern auf Festplatte vor der Allozierung
Wenn möglich, sollten Sie vor zeitkritischen oder umfangreichen Speicherallozierungen Daten auf die Festplatte zwischenspeichern. Sie könnten zum Beispiel sehr viel früher im Programm Speicher dafür allozieren. Bevor eine umfangreiche Allozierung für kritische Daten erfolgt, können Sie diesen Speicher verwenden, um Daten darin zwischenzuspeichern und auf die Festplatte zu schreiben. Im Allgemeinen gilt es, nur so viele Daten im virtuellen Speicher (RAM) zu beherbergen wie auch wirklich nötig. Eine weitere Strategie ist es, vor einer Speicherallozierung einen bereits reservierten Speicherbereich auf die Festplatte zu schreiben (temporäre Datei) und diesen Speicherblock für die nachfolgende Allozierung freizugeben. Womit Sie auch gleich beim nächsten Punkt wären.

17.9.5 Nur so viel Speicher anfordern wie nötig
Um eine optimale Speicherausnutzung zu erhalten, sollten Sie mit dem Speicher geizen wie Dagobert Duck mit seinen Talern. Wenn immer nur der benötigte Speicher oder kleine Speicherblöcke angefordert werden, erreichen Sie außerdem die beste Performance. Speicher sparen können Sie schon bei der Auswahl des Datentyps bei der Entwicklung des Programms. Benötigen Sie zum Beispiel unbedingt einen double-Wert im Programm? Reicht ein float nicht aus? Bei umfangreichen Strukturen sollten Sie sich fragen, ob alle Strukturelemente wirklich erforderlich sind. Müssen Berechnungen zwischengespeichert werden? Ein Beispiel ist der Matrixzugriff von matrix[x][y]. Das Ergebnis müsste dabei nicht gespeichert werden. Sie können auch einen Funktionsaufruf vornehmen, der Ihnen das berechnet (matrix(x, y)).

Weiter mit Kapitel 18: Strukturen            zum Inhaltsverzeichnis