C-Tutorial (C oder C++, Vorwort, Installation, Kapitel 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13)

Dynamische Speicherzuweisung, Kommandozeilenparameter

12.1. Statische Speicherzuweisung

In Kapitel 9.2 habe ich gezeigt, dass sich Strings in einem Array vom Typ char speichern lassen. Dazu hatte ich folgendes kompliziertes Beispiel:

char text[16] = { 'E','i','n',' ','l','a','n','g','e','r',' ','T','e','x','t','\0' };

Obwohl das ein sehr kurzer String mit nur 16 Zeichen (15 Zeichen Text + Nullterminierung) Länge ist, ist der Aufwand hier bereits hoch. Bei längeren Texten wird das dann erst recht zum Geduldspiel. Doch wie versprochen, geht das einfacher. Im nächsten Beispiel wird der Text hello, world! in das char-Array txt kopiert und anschließend mit printf() ausgegeben:

#include <stdio.h>
#include <string.h>
 
int main()
{
  char txt[100];
 
  /* String in txt kopieren */
  strcpy (txt, "hello, world!");
 
  printf ("%s\n", txt);
 
  return 0;
}
 

Neu ist hier zweierlei: Die Include-Datei string.h und die Funktion strcpy(). In string.h ist strcpy() deklariert, und muss deshalb eingebunden werden. strcpy() erwartet zwei char-Zeiger als Parameter: Zuerst das Ziel (wo hinkopiert werden soll) und als letztes die Quelle (von wo gelesen werden soll).

Wie Sie bereits wissen, ist der Name eines Arrays ein Zeiger auf das erste Element (siehe Kapitel 11). Daher als erster Parameter txt. Alternativ wäre auch &txt[0] möglich gewesen. Als zweiten Parameter geben wir den Text direkt an, wie schon von printf() bekannt. Hier erledigt der Compiler die restliche Arbeit.

Im Beispiel ist zur Zeit des Compilierens bereits bekannt, wieviel Speicher für den String freigegeben werden muss: Nämlich genau 100 Bytes für das 100 Elemente große char-Array. Der String ist zwar nur 14 Zeichen lang (13 Zeichen + \0 am Ende), reserviert werden aber die vollen 100 Bytes. 86 Bytes werden nicht benützt und belegen unnötig Speicher.

Im Beispiel wird ein statischer Text zugewiesen. Was, wenn man das etwas flexibler angehen möchte und eine Benutzereingabe verlangt? In Kapitel 9.2 haben wir mit fgets() einen String in ein char-Array eingelesen. fgets() ist so intelligent und beschränkt die maximal zulässige Eingabe auf den angegebenen Maximalwert, sprich wieviele Zeichen maximal eingelesen werden. Hier noch einmal das Beispiel:

#include <stdio.h>
 
int main()
{
  char eingabe[255];
 
  printf ("Text eingeben:\n");
  fgets (eingabe, sizeof(eingabe), stdin);
 
  printf ("Der Text war:\n%s", eingabe);
 
  return 0;
}

Eine kleine Änderung gibt es im Vergleich zu der Version aus Kap. 9.2: Statt die Maximallänge 255 direkt als zweiten Parameter von fgets() anzugeben, wird mit sizeof die Länge von eingabe ermittelt. Das hilft bei späteren Änderungen. Wenn Sie das Array eingabe vergrößern oder verkleinern, wird der Wert automatisch angepasst (vergleiche das ähnliche Thema "hart kodieren" vs. (symbol.) Konstanten in Kapitel 5!).

Neben fgets() existiert noch die "gefährlichere" Variante gets(), die zwar ebenfalls einen String einliest, die Maximallänge aber nicht überprüft. Geben Sie bei gets() zu viel ein, überschreiben Sie fremde Speicherbereiche (buffer overflow). Ein Programmabsturz kann die Folge sein.

Der Grund dafür ist einfach: Mehr Speicher haben Sie nicht reserviert. Diese Art, Speicher zu reservieren, nennt man Statische Speicherverwaltung. Wenn Sie das Programm compilieren, ist bereits bekannt, wieviel Speicher reserviert wird. Während das Programm läuft, haben Sie keinen Einfluss mehr darauf. Wird weniger Text als maximal möglich eingegeben, verschwenden Sie Speicherplatz. Wird zu viel eingegeben, kann das Programm abstürzen, wenn Sie Funktionen verwenden, die keine Längenüberprüfung vornehmen.

12.2. Dynamische Speicherzuweisung

Eine Lösung bietet sich an: Speicher zur Laufzeit, also dynamisch zu reservieren, die sog. Dynamische Speicherverwaltung. Während das Programm läuft, wird je nach Bedarf Speicher reserviert. Nicht benötigter Speicher kann freigegeben werden, und steht damit auch anderen Programmen wieder zur Verfügung.

Zur Speicherverwaltung stellt die Standardbibliothek vier verschiedene Funktionen zur Verfügung: malloc(), calloc(), realloc() und free(). Damit diese verwendet werden können, muss stdlib.h eingebunden werden.

Beginnen wir mit malloc(), die Sie davon vermutlich am häufigsten benötigen. malloc(), Kurzform für memory allocation, reserviert einen zusammenhängenden Speicherbereich der gewünschten Größe und liefert einen Zeiger auf den Anfang zurück. malloc() erwartet als Parameter die Größe in BYTES, die reserviert werden soll.

Das heißt, dass Sie die Größe des Datentyps berücksichtigen müssen! Damit Ihre Programme dennoch portabel bleiben, können Sie den sizeof-Operator verwenden.

Eine weitere Besonderheit gilt es zu beachten: malloc() liefert einen Zeiger vom Typ void zurück (void*)! void* ist ein typenloser Zeiger. Dieser verweist zwar auf eine Stelle im Speicher, legt aber nicht den Typ fest. Damit Ihr Zeiger den richtigen Typ erhält, benötigen Sie ein Typecast, wie Sie es schon aus Kapitel 10 kennen. Ein Beispiel:

#include <stdio.h>
#include <stdlib.h>
 
int main()
{
  int *IntegerZeiger;
 
     /* Speicher reservieren fuer einen Integer */
  IntegerZeiger = (int *) malloc( sizeof(int) );
 
  *IntegerZeiger = 12345;
  printf ("Wert des Integers: %d\n", *IntegerZeiger);
 
  return 0;
}

Im Beispiel wird ein Zeiger auf int deklariert. Bisher haben wir Zeigern immer eine Adresse einer statischen Variable zugewiesen. Sie können aber auch einem Zeiger Speicher zuweisen. Dann können Sie alles damit anstellen, was statische Variablen auch können, haben aber zusätzlich die Möglichkeiten, die Zeiger bieten. Die entscheidende Zeile ist im Beispiel:

IntegerZeiger = (int *) malloc( sizeof(int) );

Mit sizeof(int) wird die Größe eines Integers ermittelt. Auf den meisten Systemen werden das 4 Bytes sein. 4 wird dann an malloc() übergeben, und 4 Bytes werden reserviert. Den typenlosen Zeiger (void *), den malloc() liefert, müssen Sie dann noch in einen Zeiger auf int (d.h. int *) umwandeln. Das geschieht mit dem cast-Operator: (int *). Um das Ganze zu testen, wird danach der Zeiger dereferenziert, ein Wert zugewiesen und wieder ausgegeben.

Probieren Sie einmal Folgendes: Lassen Sie testweise die Zeile mit malloc() weg und testen Sie das Beispiel (vorher vorsichtshalber Daten sichern!). Unter Linux erhalten Sie dann folgende Meldung: Segmentation fault (core dumped). Unter Windows kennen Sie diesen Fall vermutlich als Schutzverletzung. Hier wurde auf einen fremden Speicherbereich zugegriffen, was das Betriebssystem verhindert hat. Linux und andere UNIX-Systeme können das Speicherabbild in einer Datei sichern (ein sog. core dump), damit Sie später Fehler suchen können. Eines ist sicher: Diese Fehlermeldung werden Sie noch öfter zu sehen bekommen! ;-)

Im Zusammenhang mit Zeigern und deren Speicherverwaltung passieren in C sehr viele Fehler. Das gilt nicht nur für Programmieranfänger! Sie sind selbst dafür verantwortlich, dass genügend Speicher zur Verfügung gestellt wird und Sie nicht auf fremde Speicherbereiche zugreifen!

Am häufigsten werden Sie vermutlich für Strings Speicher zuweisen:

char *str;
...
str = (char *) malloc ( 100 * sizeof(char) );   /* 99 Zeichen max. (effektiv) */

malloc() reserviert hier im Normalfall 100 Bytes (außer char wäre größer als 1 Byte). Der Zeiger auf das erste der 100 Bytes wird dann zurückgeliefert und str zugewiesen. Damit steht str ein 100 Byte großer Speicherbereich zur Verfügung. Nun können Sie "Ihren" Speicherbereich mit Werten füllen. Nur eines gibt es wieder zu bedenken: Achten Sie darauf, dass Sie den Bereich nicht überschreiten! Also einlesen z.B. mit fgets() oder - neu - einen String mit strncpy() kopieren:

strncpy (str, "hello, world!", 100);

Für strncpy() müssen Sie string.h einbinden. strncpy() kopiert hier maximal 100 Zeichen aus dem zweiten in den ersten String. Ist der String - wie in diesem Beispiel - kürzer, werden die restlichen Zeichen mit Nullen aufgefüllt. strncpy() sorgt aber für keine Nullterminierung (wenn der zweite String die Maximallänge hat)! Zwar wird hier kein fremder Speicherbereich gelesen, wenn Sie den String aber wieder ausgeben, kann das allerdings schon passieren, da das abschließende \0-Zeichen am Ende fehlt. Achten Sie deshalb darauf, den String zu terminieren, wie im nächsten Beispiel:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
int main()
{
  char helloWorld[] = "hello, world!";
  char *hello;
 
  hello = (char *) malloc ( 6 * sizeof(char) );
 
  strncpy (hello, helloWorld, 5);   /* kopiert nur "hello" */
  hello[5] = '\0';   /* String abschliessen! */
 
  printf ("%s\n", hello);
 
  return 0;
}

Das Beispiel zeigt am Rande, dass auch Folgendes möglich ist:

char helloWorld[] = "hello, world!";

Diese Zeile ist gleichwertig zu:

char helloWorld[]= {'h','e','l','l','o',',',' ','w','o','r','l','d','!','\0'};

Der Compiler erkennt automatisch, wie groß das Array werden muss.

Wieder zurück zum Kernthema. Neben malloc() können Sie Speicher auch mit calloc() (= core allocation) zuteilen. calloc() initialisiert jedes Byte automatisch mit 0 (Null), was einige Fehler vermeiden kann. calloc() funktioniert ähnlich wie malloc(), verlangt aber zwei Parameter: Die Anzahl der Datenobjekte und die jeweilige Größe. Statt

hello = (char *) malloc ( 6 * sizeof(char) );

ist auch

hello = (char *) calloc ( 6 , sizeof(char) );

möglich, wobei zusätzlich jedes Byte auf 0 gesetzt wird.
Nicht mehr benötigter Speicher kann mit free() wieder freigegeben werden. Z.B.:

free (hello);

Je weniger Speicher zur Verfügung steht (z.B. kleine Mikrocontroller) oder je speicherintensiver die Anwendung ist, umso wichtiger wird es, sparsam mit dem verfügbaren Speicher umzugehen. Anderenfalls wird dieser erst freigegeben, wenn das Programm beendet wird.

Wenn Sie mit malloc() oder calloc() bereits Speicher zugewiesen haben, können Sie diesen nachträglich mit realloc() (wie: memory re-allocation) vergrößern oder verkleinern. Beim Verkleinern bleibt die Anfangsadresse gleich, beim Vergrößern kann es vorkommen, dass die Daten zuerst an eine andere Stelle kopiert werden müssen. Denn: Der Speicherbereich muss zusammenhängen, damit alle Werte nacheinander liegen.

Das nächste Beispiel - das mit Abstand das längste bisher in diesem Tutorial ist - zeigt, wie Sie (theoretisch) beliebig viel Text einlesen und wieder ausgeben können. Es wird zeilenweise eingelesen, wobei je Zeile max. 80 Zeichen zulässig sind. Sie können die Eingabe beenden, indem Sie in einer Zeile nur einen Punkt . eingeben und Enter drücken.

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
 
#define MAX 80
 
char *getLine()
{
   char buffer[MAX], *zeile = 0;
   int speicherbedarf = 0;
 
      /* eine Zeile einlesen, hoechstens MAX Zeichen */
   fgets(buffer, MAX, stdin);
 
      /* hier Ende, wenn die Zeile nur aus einem Punkt besteht */
   if ( (buffer[0]=='.') && ((buffer[1]=='\n') || (buffer[1]=='\r')) )
      return NULL;   // Ende signalisieren
 
   /* sonst ... (kein else-Block noetig!) */
 
      /* Speicherbedarf des Strings ermitteln */
   speicherbedarf = strlen(buffer) + 1;    /* Stringlaenge + 1 Zeichen fuer \0 am Ende */
 
      /* Speicher reservieren */
   zeile = (char *) malloc (speicherbedarf);    /* laenge entspricht dem Speicherbedarf in Bytes! */
 
      /* Buffer kopieren */
   strncpy (zeile, buffer, speicherbedarf);
 
      /* Zeiger zurueckliefern */
   return zeile;
}
 
 
int main()
{
   char *strGes = 0, *zeile = 0;
   int bisherigerSpeicherbedarf = 0,
       zusaetzlicherSpeicherbedarf = 0;
 
      /* strGes mit Laenge 1 erstellen,
         sonst funktioniert strlen(strGes) bei der ersten Zeile nicht */
   strGes = (char *) malloc(1);
   strGes[0] = '\0';
 
   printf ("Geben Sie einen beliebigen Text ein (max. %d Zeichen/Zeile) ...\n\n", MAX);
 
   for (;;)
   {
         /* eine Zeile einlesen */
      zeile = getLine();
 
      if (zeile==NULL)  /* dann Ende der Schleife*/
         break;
 
         /* sonst Speicher vergroessern, sodass die neue Zeile Platz hat */
 
      bisherigerSpeicherbedarf = strlen(strGes) + 1;
      zusaetzlicherSpeicherbedarf = strlen(zeile);
 
      strGes = (char*) realloc (strGes, bisherigerSpeicherbedarf + zusaetzlicherSpeicherbedarf);
 
         /* neue Zeile dazukopieren */
      strcat (strGes, zeile);   /* ueberschreibt das letzte \0 */
   }
 
   printf ("\nIhre Eingabe war:\n\n%s", strGes);
 
   return 0;
}

Da das Beispiel doch deutlich länger ist als alle bisherigen, habe ich versucht, hier möglichst viel zu kommentieren und das Beispiel so übersichtlich wie möglich zu halten. Beginnen wir in der main()-Funktion:

Zunächst werden die benötigten Variablen in main() deklariert und initialisiert. Soweit nichts Neues. Danach wird strGes ein (!) Byte an Speicher zugewiesen und dieses gleich auf 0 gesetzt. Alternativ hätten Sie hier auch calloc() verwenden können. Den Grund dafür werden Sie gleich sehen.

Danach wird der Benutzer zur Eingabe aufgefordert, und es beginnt eine Endlosschleife mit for(;;). Danach folgt die entscheidende Anweisung. Dem Zeiger zeile wird von getLine() eine Adresse zugewiesen. Wie getLine() im Detail funktioniert, braucht an dieser Stelle noch nicht zu interessieren. getLine() habe ich so definiert, dass NULL zurückgeliefert wird, wenn der Benutzer nichts weiteres mehr eingeben will, also wenn er einen Punkt eingegeben hat.

Hierbei handelt es sich einfach um eine übliche Lösung. NULL ist als symbolische Konstante als typenloser Zeiger auf 0 definiert. NULL deutet an, dass der Zeiger ins Nichts zeigt. Im Beispiel heißt NULL: Beende die Schleife mit break und zeige alles auf dem Bildschirm an.

Die zwei folgenden Anweisungen sind sehr wichtig. Mit strlen() wird ermittelt, wie lang ein String ist. strlen() liefert die Länge eines Strings OHNE das abschließende \0-Zeichen zurück! Da wir \0 aber mitspeichern müssen, wird einmal 1 addiert. WICHTIG: strlen() funktioniert nur, wenn ein gültiger String übergeben wird! Das ist auch der Grund, weshalb am Anfang strGes Speicher zugewiesen wurde. Sonst erhalten Sie Segmentation fault (core dumped) oder eine vergleichbare Meldung, nachdem Sie etwas eingegeben haben. Denn von einem fremden Speicherbereich oder einem NULL-Zeiger kann strlen() nichts ermitteln.

Danach wird der Speicherplatz, den strGes benötigt, vergrößert. Die neue Größe errechnet sich aus bisherigerSpeicherbedarf und zusaetzlicherSpeicherbedarf. Damit realloc() weiß, wo der Speicherbereich anfängt, müssen Sie den Zeiger als ersten Parameter übergeben. Zurückgegeben wird ein Zeiger auf den neuen Anfangsbereich, denn der kann sich ändern, wenn Daten im Speicher verschoben werden müssen.

Die nachfolgende Zeile mit strcat() ist auch neu, aber schnell erklärt. strcat() kopiert den zweiten String an das Ende des ersten. Das \0-Zeichen des ersten Strings wird dabei überschrieben (sonst wär's ja sinnlos). Abschließend wird der eingegebene Text angezeigt.

Nun zu getLine(): Nach den Deklarationen/Initialisierungen wird mit fgets() ein String eingelesen. Soweit bekannt. Strings können Sie zeichenweise ansprechen, was wir uns in der if-Anweisung zunutze machen. Es wird überprüft, ob das erste Zeichen ein Punkt ist und das zweite ein Zeilenumbruch. Zeilenumbrüche bestehen auf Linux/UNIX-Systemen nur aus einem \n (= line feed), unter Windows aus \r (carriage return) und \n, weshalb wir hier beides erlauben. Wenn ein Punkt eingegeben und Enter gedrückt wurde, ist getLine() hier zu Ende und liefert NULL zurück.

Alles weitere ist Ihnen bereits bekannt. Nachdem Speicher reserviert und der String in den zugeteilten Speicher kopiert wurde, wird ein Zeiger auf den Anfangsbereich zurückgeliefert.

12.3. Kommandozeilenparameter verarbeiten

Zum Abschluss von Kapitel 12 möchte ich noch einen weiteren wichtigen Anwendungsbereich von Zeigern behandeln: Parameter von der Kommandozeile verarbeiten. Wir haben bisher rein textbasierte Anwendungen geschrieben. Hier gehören Kommandozeilenparameter oft zu der beliebtesten Eingabeform. Das gilt besonders für Linux-User. Aber auch als Windows-Benutzer sollten Sie diesen Abschnitt lesen. Denn hier gibt es wieder etwas Neues: Arrays, die aus Zeigern auf char bestehen.

Zunächst einmal ganz zurück zu Kapitel 1. :-)
In Kapitel 1 habe ich erwähnt, dass der Funktionskopf von main() auch anders aussehen kann. Das ist genau dann notwendig, wenn Kommandozeilen-Parameter verarbeitet werden sollen:

int main ( int argc, char *argv[] )
{
  Anweisung1;
  Anweisung2;
  ...
}

main() bekommt nun zwei Parameter: argc und argv. Die müssen Sie zwar nicht zwingend so nennen, das ist aber üblich. argc gibt an, wie viele Parameter übergeben wurden. Als ersten Parameter (Index 0!) erhalten Sie den Namen des Programms selbst, argc liefert also mindestens die Anzahl 1.

argv ist ein Array, und zwar eines, wo jedes Element ein Zeiger auf char ist. argv liefert Ihnen die Adressen im Speicher, an denen die Eingaben gespeichert sind, die der Benutzer getätigt hat.

Testen Sie das folgende Beispiel. Sie können beliebig viele Parameter angeben. Das Programm zeigt Ihnen die Anzahl und die einzelnen Eingaben an. Die einzelnen Parameter werden durch Leerzeichen getrennt. Um das zu verhindern, müssen Sie Parameter auf der Kommandozeile/Shell unter Anführungszeichen "..." setzen.

Wenn Sie Code::Blocks verwenden, können Sie auch direkt über dessen IDE die Parameter festlegen. Dazu müssen Sie ein neues Projekt erstellen. Klicken Sie auf File->New->Project und wählen Sie im Assistenten Console Application. Legen Sie anschließend C als Sprache und einen beliebigen Projekttitel fest. Alles weitere können Sie übernehmen.

Fügen Sie nun ein neues File zu Ihrem Projekt hinzu. Über Project->Add files... können Sie eine bereits existierende .c-Datei zum Projekt hinzufügen. Alternativ können Sie auch eine neue Quellcodedatei erstellen. Wählen Sie dann im linken Management-Fenster unter Sources Ihre .c-Datei aus. Nun können Sie über Project->Set programs' arguments die Kommandozeilenparameter im Eingabefeld Program arguments: bekannt geben. Diese müssen untereinander angegeben werden.

#include <stdio.h>
 
int main ( int argc, char *argv[] )
{
  int i;
 
  printf ("Es wurden %d Parameter angegeben.\n\n", argc);
 
  for (i = 0; i < argc; i++)
    printf ("argv[%d] = %s\n", i, argv[i]);
 
  return 0;
}

Um die Eingaben mit den von Ihnen geforderten Werten zu vergleichen, können Sie die Funktion strcmp() verwenden, die in string.h deklariert ist. strcmp() erwartet beide Strings als Parameter und liefert 0 (Null) zurück, wenn beide gleich sind.

Vorheriges Kapitel Nächstes Kapitel