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

Arrays und Strings

9.1. Arrays (Felder) erstellen

Angenommen, Sie wollen 50 Werte (Telefonnummern, Adressen, was auch immer ..) erfassen, die der Benutzer eingeben soll. Dabei treten zwei Probleme auf: 1.) 50 Variablen von einem bestimmten Typ zu deklarieren, ist mühsam. 2.) Man benötigt hierfür 50 (!) verschiedene scanf()-Anweisungen, um in die Variablen Werte einzulesen. Oder alternativ 50 Formatelemente. ;-)

Das klingt natürlich recht unpraktisch. Aber natürlich gibt es eine Lösung, hier systematisch vorzugehen. Erster Versuch. Eine erste Idee wäre, eine Schleife einfach 50-mal durchlaufen zu lassen. Pro Schleifendurchgang lesen Sie einen Wert ein und speichern ihn in einer anderen Variable ab. So einfach geht das.

Für die Variablen überlegen Sie sich ein einheitliches Format, z.B. Telefonnummer1 bis Telefonnummer50. Geht das? Ja - nur nicht in C! In manchen Sprachen - wie etwa PHP - ist es möglich, Variablen dynamisch zur Laufzeit zu erzeugen, also auch Variablennamen zusammenzusetzen.

Da diese Lösung aber weder schön ist, noch in C funktioniert, müssen wir sie verwerfen. Um's nicht allzu spannend zu machen: Es lässt sich mit einem Array lösen. Das ist die in Hochsprachen übliche Lösung. Ein Array (auch (Daten)-Feld oder Vektor genannt) dient zur Speicherung größerer Datenmengen des gleichen Datentyps.

Ein Array fasst quasi mehrere Variablen des gleichen (!) Typs unter einem Namen zusammen. Sie können, um dieses Beispiel zu lösen, also ein Array mit 50 Elementen erstellen. Um auf ein einzelnes Element zuzugreifen, benötigen Sie neben dem Namen (Bezeichner) des Arrays den Index des Elements.

Wichtig: Die Nummerierung beginnt bei 0 (Null) in C! Das erste Element hat den Index 0, das zweite den Index 1 und das letzte (Anzahl der Elemente minus 1), also im Beispiel 49. Ein Array kann überall dort deklariert werden, wo es auch für Variablen zulässig ist.

Ein einfaches Array, bestehend aus fünf Integern:

int Feld[5];

Hiermit wird das Array mit dem Namen Feld deklariert. Feld hat 5 Elemente, was die Zahl in eckigen Klammern [ ] angibt. Alle fünf Elemente sind vom Typ int. Auf ein Element kann über den Index in eckigen Klammern zugegriffen werden. Das erste Element ist dabei

Feld[0]

und das letzte ist

Feld[4]

Nun ist es an der Zeit, das in einem Beispiel zu verpacken. Das folgende Programm liest maximal 50 Zahlen in ein Array ein, wobei Sie durch Eingabe von 0 signalisieren können, dass Sie keine weiteren Zahlen eingeben möchten. Anschließend werden die Zahlen ausgegeben. Allerdings nur die Elemente, in denen keine 0 steht.

#include <stdio.h>
 
#define ANZAHL 50
 
int main()
{
  int eingaben[ANZAHL], i;
 
  for (i = 0; i < ANZAHL; i++)
    eingaben[i] = 0;  /* jedes Element mit 0 initialisieren */
 
  for (i = 0; i < ANZAHL; i++) /* Werte einlesen */
  {
    printf ("%d. Element: ", i);
 
    scanf ("%d", &eingaben[i]);
 
    if (eingaben[i] == 0)
    {
      printf ("\n--------- Ihre Eingabe: ---------\n");  /* dann schaut es übersichtlicher aus */
      break; /* bei 0 die Schleife abbrechen */
    }
  }
 
  for (i = 0; i < ANZAHL; i++) /* Werte ausgeben */
  {
    if (eingaben[i] != 0)  /* nur dann, wenn der Wert nicht Null ist, diesen ausgeben */
      printf ("%d. Element: %d\n", i, eingaben [i]);
    else   /* ansonsten Schleife abbrechen */
      break;
  }
 
  return 0;
}

Das Beispiel zeigt gleich einen Anwendungsfall für eine symbolische Konstante. Gleich zu Beginn wird mit #define die Anzahl der Elemente definiert. Wollen Sie das Beispiel nachträglich abändern, etwa weil sie mehr oder weniger Elemente brauchen, müssen Sie nur eine Zeile ändern! Denn statt dass 50 direkt im Quellcode vorkommt ("hart kodiert"), steht jeweils ANZAHL. Der aktuelle Wert von ANZAHL wird vom Präprozessor automatisch überall statt ANZAHL eingesetzt.

Hier ist wichtig zu erwähnen: Es ist in ANSI-C nicht erlaubt, als Index eine Konstante anzugeben, die mit const deklariert wurde! Mit GCC funktioniert das zwar trotzdem, mit anderen Compilern aber i.d.R. nicht! Verwenden Sie #define, um Ihre Programme plattformunabhängig zu gestalten.

Am Anfang der main()-Funktion steht die Deklaration des Arrays: eingaben[ANZAHL]. Das Array verhält sich, lokal deklariert, ähnlich einer Variablen. Das heißt, die einzelnen Elemente können einen nicht-vorhersehbaren Wert haben. Darum werden in der ersten Schleife alle Elemente auf 0 gesetzt - und weil das zugleich unsere Abbruchbedingung ist. Die Schleife läuft von 0 bis i KLEINER ANZAHL, also von 0 bis inklusive 49. Index 49 ist bekanntlich das 50. und letzte Element.

Achten Sie darauf, dass Sie den zulässigen Bereich nicht überschreiten! Tun Sie das doch, etwa wenn Sie auf das 51. Element (Index 50) zugreifen, ist das Verhalten undefiniert. Es kann nichts passieren, wie etwa bei meinen Testläufen mit GCC, üblicher sind jedoch Laufzeitfehler bzw. Programmabstürze. Denn es kann sein, dass Sie auf fremde Bereiche im Arbeitsspeicher zugreifen, bzw. es versuchen, was moderne Betriebssysteme aber verhindern.

Im restlichen Programmablauf gibt es nichts wesentlich Neues. Einzig der Zugriff auf das Array über eingaben[i] ist Ihnen noch nicht vertraut.

Ein wichtiger Punkt bei lokal deklarierten Arrays ist deren Initialisierung. Ein Beispiel:

int daten[3] = { 0, 0, 0 };

Hier wird das Array daten mit 3 Elementen vom Typ int deklariert und zugleich definiert. Alle drei Elemente werden auf 0 gesetzt (initialisiert). Die einzelnen Werte werden dabei der Reihe nach zwischen geschwungenen Klammern { } angegeben.

int daten[] = { 4, 10, -20 };

Hier wurde die Anzahl der Elemente in eckigen Klammern weggelassen. Das ist immer dann möglich, wenn ein Array gleich initialisiert wird. Denn es geht für den Compiler aus der Initialisierungsliste ohnehin hervor, wieviel Speicher und Elemente reserviert werden müssen.

Das erste Element (daten[0]) wird mit 4 initialisiert, das zweite (daten[1]) mit 10 und das dritte (daten[2]) und zugleich letzte mit -20.

Ohne es explizit zu erwähnen, waren alle bisherigen Arrays eindimensional. Ein Array mit 5 Elementen könnte man grafisch folgendermaßen veranschaulichen:

[0] [1] [2] [3] [4]

Das Array besitzt 5 Elemente. Nimmt man eine weitere Dimension hinzu, und soll das Array auch in der zweiten Dimension 5 Elemente besitzen, so stehen insgesamt 5 * 5 Elemente zur Verfügung:

[0][0] [0][1] [0][2] [0][3] [0][4]
[1][0] [1][1] [1][2] [1][3] [1][4]
[2][0] [2][1] [2][2] [2][3] [2][4]
[3][0] [3][1] [3][2] [3][3] [3][4]
[4][0] [4][1] [4][2] [4][3] [4][4]

Um auf eines der Elemente zuzugreifen, muss der Index beider Dimensionen angegeben werden. Die nächste Tabelle zeigt das gleiche Array, allerdings steht der Index in der Tabellenüberschrift (horizontal und vertikal) und in der Tabelle die einzelnen Werte. Ich habe beliebige Werte ausgesucht.

       0 1 2 3 4
0 34 23 134 -1900 139
1 17 3 -20 -1500 1
2 3 2 -11 -990 123
3 -3 100 1000 4 44
4 -12 45 68 779 12

Die fett geschriebenen Zahlen stellen den Index beider Dimensionen dar. Nehmen wir an, dass die Zahlen in Richtung der x-Achse (verläuft horizontal) die erste Angabe darstellen und die in der y-Achse (verläuft vertikal) den zweiten Wert. Wenn man das so betrachtet, so beziehen sich die Indizes [2][3] auf die Zahl 1000. Betrachtet man die Tabelle anders herum, so könnte es allerdings auch die Zahl -990 sein. Aber das ist ja nur eine Darstellungssache.

Ein mehrdimensionales Array wird ähnlich einem eindimensionalen deklariert. Wichtig ist hierbei nur, dass alle Dimensionen angegeben werden. Ein kleines Beispiel für ein dreidimensionales Array:

int dreidimensional [3][4][34];

Das Array enthält insgesamt 3 * 4 * 34 = 408 (!) Elemente. Auf ein mehrdimensionales Array wird ähnlich wie auf ein eindimensionales zugegriffen. Es ist wiederum wichtig, jede Dimension anzugeben. Mit

dreidimensional [1][3][30] = 409;

weisen Sie dem Element dreidimensional[1][3][30] den Wert 409 zu. Das Komplizierteste bei mehrdimensionalen Arrays ist zweifellos deren Initialisierung.

Da dreidimensionale Arrays schwer zu veranschaulichen (wäre ein dreidimensionaler Quader) sind, gehen wir einen Schritt zurück zu zwei Dimensionen. Im Folgenden wird das Array daten[3][2] deklariert, das somit 6 (= 2 * 3) Elemente enthält. Das Array wird bei der Deklaration vollständig initialisiert:

int daten [3][2] = { { 1, 2 } , { 3, 4 } , { 5, 6 } };

Wie bei einem eindimensionalen Array, ist die Initialisierung von geschwungenen Klammern umgeben (blau gekennzeichnet). Abschließend folgt ein Semikolon. An dem ersten Index kann man erkennen, dass in der ersten Dimension 3 Ebenen existieren: daten[0][..], daten[1][..] und daten[2][..]. Jede dieser Dimensionen muss wiederum in geschwungenen Klammern stehen (rot gekennzeichnet). Beachten Sie, dass diese durch Beistriche getrennt werden müssen! Schlussendlich fügen Sie in jede der 3 Dimensionen 2 Werte ein (grün gekennzeichnet). Da die zweite Dimension aus zwei Elementen besteht, müssen zwei Werte dort stehen. Das Array wurde damit komplett initialisiert.

Was wurde nun womit initialisiert? Die Zählung beginnt wie üblich mit 0. Das heißt, der Wert 1 gehört zu daten[0][0], 2 gehört zu daten[0][1], 3 zu daten[1][0], 4 zu daten[1][1], 5 zu daten[2][0] und 6 zu daten[2][1].

Abschließend (doch) noch ein dreidimensionales Array:

int d3 [3][2][4] = {
                      { {1,2,3,4}, {5,6,7,8} } ,
                      { {9,10,11,12}, {13,14,15,16} } ,
                      { {17,18,19,20}, {21,22,23,24} }
                    };

Der Übersichtlichkeit halber habe ich hier eingerückt. Ziemlich leicht erkennbar sind die 3 Ebenen der ersten Dimension. Jede Ebene besteht wiederum aus 2 weiteren (2. Dimension). Und jede 2. Dimension speichert 4 Werte (3. Dimension). So hat beispielsweise d3[1][1][2] den Zahlenwert 15.

9.2. Strings

Unter einem String versteht man eine Zeichenkette, also eine Folge von Zeichen. Ein Text also. Zum Verständnis der internen Speicherung und dem richtigen Umgang mit Strings, ist das Thema Arrays von Bedeutung, weshalb es im vorigen Abschnitt ausführlich(er) behandelt wurde.

Zunächst geht es um einzelne Zeichen (ASCII-Zeichen), für die man üblicherweise den Typ char zur Speicherung verwendet. char (wie character) gehört zu den Ganzzahlentypen, ebenso wie int, short und long. Das heißt, char speichert nur Zahlen! Doch wie kommt es von der Zahl zum Zeichen? Im Grunde erfolgt jede Datenspeicherung binär, d.h. als Folge von Binärzahlen. Um welche Art von Information es sich handelt, ist egal. Entscheidend ist die Darstellung der Daten.

Damit ein als Zahl gespeicherter Buchstabe auch wieder richtig angezeigt wird, gibt es Zeichensätze wie den ASCII-Code. Jedem Zeichen ist eine Zahl zugeordnet bzw. umgekehrt. So hat das kleine a etwa den ASCII-Code 97 (dezimal), b hat 98, c hat 99 usw. Nun wird für a die Zahl 97 abgespeichert.

Mit printf() können Sie sehr direkt über die Darstellung entscheiden. Sie können den Wert einer char-Variable auf verschiedene Weisen ausgeben. Entweder, Sie lassen sich mit %d den Zahlenwert anzeigen (d.h. Interpretation als Integer), oder mit %c das entsprechende ASCII-Zeichen.

Das nächste Beispiel ermöglicht, dass ein Wert (ein Zeichen) eingelesen und anschließend wieder ausgegeben wird. Zusätzlich soll der ASCII-Code angezeigt werden. Dazu muss eine Variable vom Typ char deklariert werden, die das eingelesene Zeichen speichert. int oder long wären auch möglich, sind aber unnötiger Speicherverbrauch. Anschließend wird mit scanf() ein Zeichen eingelesen. Dabei kommt das Formatelement %c zum Einsatz. Die Ausgabe erfolgt mit printf() mit %d für den dezimalen Zahlenwert und %c für das entsprechende Zeichen.

#include <stdio.h>
 
int main()
{
  char zeichen;
 
  printf ("Ein Zeichen eingeben: ");
  scanf ("%c", &zeichen);
 
  printf ("Zeichen: %c, ASCII-Code: %d.\n", zeichen, zeichen);
 
  return 0;
}

Doch nun zu den eigentlichen Strings. Ein String ist in C ein Array vom Typ char! Andere Programmiersprachen gehen hier andere Wege. Das Array kann somit im Ganzen eine Zeichenkette speichern. Jedes Element des Arrays ist für ein Zeichen zuständig. Die einzelnen Zeichen werden im Arbeitsspeicher nacheinander (WICHTIG!) abgespeichert. Das Ende eines Strings wird durch ein spezielles Zeichen mit dem ASCII-Code 0 gekennzeichnet. Das Zeichen erhält man über die Escape-Sequenz \0. Man spricht von Null-terminierten Strings bzw. von einer Null-Terminierung.

Es gibt zwei Möglichkeiten, um einen String zu deklarieren: 1.) Mit einem char-Array, was Thema dieses Kapitels ist. 2.) Mit einem Zeiger vom Typ char *. Letzteres wird in Kapitel 11 (Zeiger) behandelt.

Wichtig ist, dass das Zeichen \0 auch ein Byte Speicherplatz belegt - wie jedes andere Zeichen! Wenn Sie einen 15 Zeichen langen Text speichern möchten, müssen Sie also ein Array mit 16 (!) Elementen deklarieren:

char text[21];

Es ist möglich, das Array direkt bei dessen Deklaration zu initialiseren, dh. bei der Deklaration schon festzulegen, welchen Text es enthalten soll. Aber auch später kann ein Wert - etwa mittels scanf() - zugewiesen werden. Die Initialisierung unterscheidet sich nicht von der eines Arrays mit einem anderem Typ:

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

Beachten Sie das abschließende \0 - Zeichen! Bei einem nur 15 Zeichen langen Text ist das schon eine ganze Menge Arbeit. Hier gibt es zum Glück eine einfachere Lösung, die Sie in Kapitel 11 mit Wissen über Zeiger kennen lernen werden.

Nun machen wir uns daran, mit scanf() einen String in ein Array einzulesen. Neu ist das Formatelement %s. Beachten Sie des Weiteren, dass vor dem Bezeichner des char-Arrays KEIN & stehen darf! Warum, sehen Sie auch in Kapitel 11 über Zeiger ...

Das nächste Beispiel demonstriert, wie ein bis zu 255 Zeichen langer String in ein char-Array eingelesen und wieder ausgegeben werden kann:

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

Geben Sie testweise einen Text ein, der aus mehreren Wörtern besteht. Wenn Sie nun etwa Dies ist mein Text eingeben, erhalten Sie als Ausgabe: Dies. Der Grund dafür liegt bei scanf(), denn scanf() liest immer nur bis zum Auftreten des ersten Leerzeichens bzw. bis zu einem Zeilenumbruch (\n) ein. Das Leerzeichen, das nach Dies folgt, wurde uns hier zum Verhängnis.

Abhilfe schafft hier die Funktion fgets(), die in stdio.h deklariert ist. stdio.h muss also eingebunden werden. Als Parameter wird - wie im nächsten Beispiel gezeigt - das char-Array angegeben (wieder ohne &!). Daneben verlangt fgets() die maximale Länge des Arrays sowie den Stream, von dem gelesen werden soll. Letzteres werden Sie im Zusammenhang mit Dateien noch kennen lernen. :-)
Geben Sie hier stdin an, um von der Tastatur zu lesen. Das letzte Beispiel in "funktionstüchtiger" Form:

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