9    Arrays und Zeichenketten (Strings)

Bisher haben Sie mit Datentypen gearbeitet, die nur eine Dimension hatten. In diesem Kapitel erfahren Sie nun etwas über Datenfelder (Arrays) und Zeichenketten (Strings).

Bisher haben Sie sich auf einfache Datentypen beschränkt. Auch bei den Übungsaufgaben wurden lediglich ganze Zahlen (char, short, int, long, long long) bzw. Fließkommazahlen (float, double, long double) verwendet. In diesem Kapitel erfahren Sie nun etwas über Datenfelder, also Arrays. Zu den Arrays werden in C auch die Zeichenketten (Strings) gezählt. Wenn das Array beispielsweise vom Typ T ist, spricht man von einem T-Array. Ist das Array vom Typ long, spricht man von einem long-Array.

9.1    Was genau sind Arrays?

Arrays dienen dazu, mehrere Variablen gleichen Typs zu speichern. Bei Arrays werden hierzu die einzelnen Elemente als eine Folge von Werten eines bestimmten Typs im Speicher abgelegt. Wenn Sie z. B. ein Array mit 3*3*3 Werten definieren, dann werden 27 Werte hintereinander im Speicher abgelegt. Deshalb werden Arrays auch schon mal – fälschlicherweise – als »Vektoren« oder »Reihen« bezeichnet.

Aber Achtung! Ein Vektor ist beispielsweise auch ein mathematisches Konstrukt und in C kein Standard-Speicherobjekt. In anderen Sprachen gibt es dagegen Vektoren als Objekte, die sich aber von Arrays unterscheiden. Eine Reihe dagegen (auch als Folge bezeichnet) ist kein Datenfeld, sondern stellt zumindest in der Mathematik oft eine Summe unendlich vieler Werte dar und kann z. B. innerhalb eines Algorithmus gegen einen bestimmten Grenzwert konvergieren.

Deshalb sollten Sie Arrays besser mit einer Matrize (oft auch Matrix genannt) gleichsetzen. In einer Matrix greifen Sie auf die einzelnen Elemente genau so zu wie auf ein C-Array, nämlich durch die Angabe von Indizes. Dabei kann eine Matrix, ebenso wie ein Datenfeld, durchaus nur eine einzige Zeile haben. Dies ist z. B. bei Strings der Fall.

9.1.1    Arrays definieren

Die allgemeine Syntax zur Definition eines Arrays sieht wie folgt aus:

Datentyp Arrayname[Anzahl_der_Elemente]; 

Als Datentyp geben Sie an, von welchem Typ die Elemente des Arrays sein sollen. Der Array-Name ist frei wählbar, mit denselben Einschränkungen für Bezeichner wie bei Variablen (siehe Abschnitt 2.4.1, »Bezeichner«).

Mit Anzahl_der_Elemente wird die Anzahl der Elemente angegeben, die im Array gespeichert werden können. Der Wert in den eckigen Klammern muss eine ganzzahlige Konstante oder ein ganzzahliger kontanter Ausdruck größer als 0 sein. Arrays, die aus Elementen unterschiedlicher Datentypen bestehen, gibt es in C nicht. Bei der Anzahl der Elemente können Sie durchaus auch mehrere Dimensionen angeben. Hierzu müssen Sie die eckigen Klammern mehrfach verwenden. Wenn Sie z. B. eine Art Würfel mit 3 Dimensionen à 3 Integer-Werten definieren wollen, so müssen Sie dies wie folgt tun:

int Wuerfel[3][3][3]; // 3 Dimensionen mit jeweils 3 Werten 

[»]  Array mit variabler Länge (VLA)

Seit dem C11-Standard sind Variablen für die Anzahl der Elemente in den eckigen Klammern nur als optionale Erweiterung erlaubt, werden aber nicht mehr gefordert, wie dies noch in C99 der Fall war. Dies kann dazu führen, dass manche nach 2011 entwickelte Compiler den Versuch, Variablen als Array-Indizes zu benutzen, mit einer Fehlermeldung quittieren. In diesem Fall müssen Sie auf dynamische Speicherverwaltungsfunktionen wie malloc() zurückgreifen und Speicher zur Laufzeit reservieren. Wie dies funktioniert, erfahren Sie in Kapitel 11, »Dynamische Speicherverwaltung«.

Zugreifen können Sie auf das gesamte Array mit allen Komponenten über den Array-Namen. Die einzelnen Elemente eines Arrays werden durch den Array-Namen und einen Indexwert zwischen eckigen Klammern angesprochen. Der Indexwert selbst wird über eine Ordinalzahl (Ganzzahl) angegeben und fängt bei 0 an zu zählen. In C können Sie dies auch nicht ändern wie in anderen Programmiersprachen.

Nehmen wir als Beispiel folgendes Array:

int iArray[8]; 

Das Array hat den Bezeichner iArray und besteht aus acht Elementen vom Typ int. In diesem Array können Sie also acht Integer-Werte abspeichern. Intern wird für dieses Array somit automatisch Speicherplatz für acht Werte vom Typ int reserviert. Wie viel Speicher dies ist, können Sie mit 8*sizeof(int) ermitteln. Hat der Typ int auf Ihrem System eine Breite von 4 Bytes, ergibt dies 32 Bytes (8 Elemente × 4 Bytes pro Element = 32).

9.1.2    Arrays mit Werten versehen und auf sie zugreifen

Um einzelnen Array-Elementen einen Wert zu übergeben oder Werte aus ihnen zu lesen, wird der Indizierungsoperator [] (auch Subskript-Operator genannt) verwendet. Wie der Name schon sagt, können Sie damit auf ein Array-Element mit einem Index zugreifen.

Listing 9.1 zeigt ein einfaches Beispiel:

00  // Kapitel9/arraydefinition.c
01 #include <stdio.h>
02 #include <stdlib.h>

03 int main(void) {
04 int iArray[3] = {0};
05 // Array mit Werten initialisieren
06 iArray[0] = 1234;
07 iArray[1] = 3456;
08 iArray[2] = 7890;

09 // Inhalt der Array-Elemente ausgeben
10 printf("iArray[0] = %d\n", iArray[0]);
11 printf("iArray[1] = %d\n", iArray[1]);
12 printf("iArray[2] = %d\n", iArray[2]);
13 return EXIT_SUCCESS;
14 }

Listing 9.1     Ein Array definieren und auf die Elemente zugreifen

In den Zeilen (06) bis (08) wurden je drei Array-Elementen Werte mithilfe des Indizierungsoperators und der entsprechenden Indexnummer zugewiesen. In den Zeilen (10) bis (12) wird dies umgekehrt gemacht: Hier werden also einzelne Werte ausgegeben.

Ihnen dürfte wahrscheinlich gleich auffallen, dass in Zeile (04) ein Array mit der Ganzzahl 3 zwischen dem Indizierungsoperator definiert wurde, aber weder bei der Zuweisung in den Zeilen (06) bis (08) noch bei der Ausgabe in den Zeilen (10) bis (12) vom Indexwert 3 Gebrauch gemacht wurde. Darüber stolpern viele Einsteiger: Das erste Element in einem Array muss nämlich immer die Indexnummer 0 haben. Wenn das erste Element in einem Array den Index 0 hat, besitzt das letzte Element logischerweise den Index n-1 (n ist die Arraygröße). In C gibt es wie gesagt keine Möglichkeit, Arrays mit dem Index 1 anfangen zu lassen, wie es in anderen Programmiersprachen z. B. mit der OPTION BASE-Anweisung möglich ist.

Unser Array iArray mit drei Elementen vom Datentyp int aus dem Beispiel arraydefinition.c können Sie sich so wie in Abbildung 9.1 vorstellen.

Hätten Sie im Programm arraydefinition.c folgende Zeile hinzugefügt,

  ...
iArray[3] = 6666;
...
printf("iArray[3] = %d\n", iArray[3]);
Dies ist ein Array mit drei Elementen, das mit Werten initialisiert wurde.

Abbildung 9.1     Dies ist ein Array mit drei Elementen, das mit Werten initialisiert wurde.

so würden Sie auf einen nicht geschützten und reservierten Speicherbereich zugreifen. Bestenfalls stürzt das Programm gleich mit einer Schutzverletzung (engl. segmentation fault) oder einer Zugriffsverletzung (engl. access violation) ab. Schlimmer ist es aber, wenn das Programm weiterläuft und irgendwann eine andere Variable diesen Speicherbereich, der ja nicht reserviert und geschützt ist, verwendet und ändert. Sie erhalten dann unerwünschte Ergebnisse bis hin zu einem schwer auffindbaren Fehler im Programm. In Abbildung 9.2 sehen Sie eine grafische Darstellung der Schutzverletzung des Speichers.

Mithilfe des Indizierungsoperators wurde auf einen nicht geschützten Bereich zugegriffen, was eine Schutzverletzung darstellt. Das weitere Verhalten des Programms ist undefiniert.

Abbildung 9.2     Mithilfe des Indizierungsoperators wurde auf einen nicht geschützten Bereich zugegriffen, was eine Schutzverletzung darstellt. Das weitere Verhalten des Programms ist undefiniert.

[»]  Array-Überlauf überprüfen

Auf vielen Systemen gibt es eine Compiler- bzw. Debugging-Option, mit der eine Schutzverletzung eines Arrays geprüft und mindestens eine Warnmeldung ausgegeben wird, wenn ein möglicher Zugriff auf einen nicht geschützten Bereich erkannt wird. Allerdings verlangsamt diese Option sehr oft den Programmablauf dramatisch und sollte nur in den ersten Testläufen verwendet werden (oder wenn es nicht auf Geschwindigkeit ankommt).

Beispiele wie arraydefinition.c aus Listing 9.1 sind ziemlich trivial. Häufig werden Sie Werte von Arrays in Schleifen übergeben oder auslesen. Hierbei kann es schnell mal zu einem Über- bzw. Unterlauf kommen, wenn Sie nicht aufpassen. Listing 9.2 zeigt ein einfaches und typisches Beispiel dazu:

00  // Kapitel9/array_initialisieren.c
01 #include <stdio.h>
02 #include <stdlib.h>
03 #define MAX 10

04 int main(void) {
05 unsigned int iArray[MAX];
06 // Werte an alle Elemente
07 for(unsigned int i = 0; i < MAX; i++) {
08 iArray[i]=i*i;
09 }
10 // Werte ausgeben
11 for(unsigned int i = 0; i < MAX; i++) {
12 printf("iArray[%d] = %d\n", i, iArray[i]);
13 }
14 return EXIT_SUCCESS;
15 }

Listing 9.2     »array_initialisieren.c« weist den einzelnen Array-Elementen in einer Schleife Werte zu, was sehr oft die Methode der Wahl ist.

In Listing 9.2 wird nichts anderes gemacht, als dem Array iArray mit MAX-Elementen vom Typ int in der for-Schleife (Zeile (07) bis (09)) Werte einer Multiplikation zuzuweisen. Diese Werte werden in den Zeilen (11) bis (13) auf ähnlichem Weg wieder ausgegeben.

Das Programm gibt bei der Ausführung Folgendes aus:

iArray[0] = 0
iArray[1] = 1
iArray[2] = 4
iArray[3] = 9
iArray[4] = 16
iArray[5] = 25
iArray[6] = 36
iArray[7] = 49
iArray[8] = 64
iArray[9] = 81

In einer solchen for-Schleife sollten Sie immer darauf achten, dass es nicht zu einem Über- bzw. Unterlauf des Arrays kommt. Falls Sie z. B. vergessen, dass das erste Element eines Arrays mit dem Index 0 beginnt, machen Sie unter Umständen den folgenden Fehler:

unsigned iArray[10];
...
for(unsigned int i = 0; I <= 10; i++) {
iArray[i]=i;
}

Durch die Verwendung des <=-Operators statt des <-Operators werden jetzt 11 anstelle von 10 Array-Elementen initialisiert. Damit haben Sie einen Array-Überlauf erzeugt, und das Programm verhält sich in einer nicht vorherzusehenden Weise. Gleiches gilt bei einem Unterlauf eines Arrays, wenn Sie den Array-Index beispielsweise rückwärts durchlaufen. Auch hier müssen Sie dafür sorgen, dass kein negativer Indexwert für ein Array verwendet wird. Auch in diesem Fall wäre sonst das Verhalten des Programms nicht vorhersehbar, was zu schwer auffindbaren Fehlern führen kann. Deshalb lautet unser Tipp: Suchen Sie immer, wenn sich Ihr Programm merkwürdig verhält, zuerst nach Über- oder Unterläufen von Zahlenbereichen und Arrays.

9.1.3    Initialisierung mit einer Initialisierungsliste

Ein Array können Sie bereits bei der Definition mit einer Initialisierungsliste initialisieren. (Das gilt vor allem für kleine Arrays.) Bei dieser Art der Initialisierung geben Sie in geschweiften Klammern eine Liste von Werten an, die durch Kommata voneinander getrennt sind. Ein einfaches Beispiel ist:

float fArray[3] = { 0.75, 1.0, 0.5 }; 

Nach dieser Initialisierung haben die einzelnen Elemente im Array fArray folgende Werte:

fArray[0] = 0.75
fArray[1] = 1.0
fArray[2] = 0.5

In der Definition eines Arrays mit Initialisierungsliste kann auch die Längenangabe fehlen. So ist die folgende Definition gleichwertig mit der obigen:

// float-Array mit drei Elementen
float fArray[] = { 0.75, 1.0, 0.5 };

Geben Sie hingegen bei der Längenangabe einen größeren Wert an, als Elemente in der Initialisierungsliste vorhanden sind, haben die restlichen Elemente in der Liste automatisch den Wert 0. Wenn Sie mehr Elemente angeben, als in der Längenangabe definiert sind, werden die zu viel angegebenen Werte in der Initialisierungsliste einfach ignoriert. Manche Compiler, wie z. B. Borland C++, geben hier allerdings eine Fehlermeldung wie array elements exceed boundary aus.

Wir zeigen Ihnen nun noch ein Beispiel zur Array-Initialisierung:

long lArray[5] = { 123, 456 }; 

Hier wurden nur die ersten beiden Elemente in der Liste initialisiert. Die restlichen drei Elemente haben automatisch den Wert 0. Nach der Initialisierung haben die einzelnen Elemente im Array lArray also folgende Werte:

lArray[0] = 123
lArray[1] = 456
lArray[2] = 0
lArray[3] = 0
lArray[4] = 0

Somit können Sie davon ausgehen, dass Sie die Werte aller Elemente eines lokalen Arrays mit dem Wert 0 initialisieren können:

// Alle 100 Array-Elemente mit 0 initialisiert
int iarray[100] = { 0 };

Ohne explizite Angabe einer Initialisierungsliste werden die einzelnen Elemente nur bei globalen oder static-Arrays automatisch vom Compiler mit 0 initialisiert:

// Alle 100 Array-Elemente automatisch mit 0 initialisiert
static int iarray[100];

9.1.4    Bestimmte Elemente direkt initialisieren

Sie haben auch die Möglichkeit, ein bestimmtes Element bei der Definition zu initialisieren. Hierzu müssen Sie in der Initialisierungsliste lediglich das gewünschte Element in eckigen Klammern angeben, etwa so:

#define MAX 5
int iArray[MAX] = { 123, 456, [MAX-1] = 789 };

Hier wurden die ersten beiden Elemente initialisiert; anschließend wurde dem letzten Element in der Liste ein Wert zugewiesen. Nach der Initialisierung haben die einzelnen Elemente im Array iArray folgende Werte:

iArray[0] = 123
iArray[1] = 456
iArray[2] = 0
iArray[3] = 0
iArray[4] = 789

9.1.5    Arrays mit Schreibschutz

Wenn Sie ein Array benötigen, bei dem die Werte schreibgeschützt sind und nicht mehr verändert werden sollen, können Sie das Schlüsselwort const vor die Array-Definition setzen. Die Werte in der Initialisierungsliste können so nicht mehr aus Versehen geändert und überschrieben werden. Ein einfaches Beispiel dazu sieht so aus:

#define RGB 3
// Ein konstantes Array kann zur Laufzeit nicht geändert werden.
const unsigned int gelb[RGB] = { 255, 255, 0 };
// Fehler!!! Zugriff auf konstantes Array nicht möglich
gelb[2] = 20;

9.1.6    Arrays mit fester und variabler Länge (VLA)

Seit dem C99-Standard (also quasi auf allen zurzeit verfügbaren Compilern) wurde ebenfalls eingeführt, dass bei der Definition die Anzahl der Elemente kein konstanter Ausdruck mehr sein muss.

Seit dem C11-Standard ist die Unterstützung von VLA (Variable Length Arrays) allerdings nur noch optional vorgeschrieben. Trotzdem soll sie hier kurz beschrieben werden, obwohl sie eben nicht immer unterstützt wird. Die beste Chance, VLA zu nutzen, haben Sie hier übrigens, wenn Sie einen C++-Compiler verwenden. Mit dem Makro __STDC_NO_VLA__ können Sie ferner testen, ob VLA überhaupt unterstützt wird.

Die Voraussetzung dafür, dass die Anzahl der Elemente kein konstanter Ausdruck sein muss, ist, dass das Array eine lokale Variable ist und nicht mit dem Spezifizierer static gekennzeichnet ist. Das Array muss außerdem in einem Anweisungsblock definiert werden. Listing 9.3 zeigt ein Beispiel:

00  // Kapitel9/vla_arrays.c
01 #include <stdio.h>
02 #include <stdlib.h>
03 #if __STDC_NO_VLA__
04 #error "No VLA support!"
05 #endif

06 int main(void) {
07 int val = 0;
08 printf("Anzahl der Elemente: ");
09 if( scanf("%d", &val) != 1 ) {
10 printf("Fehler bei der Eingabe ...\n");
11 return EXIT_FAILURE;
12 }
13 if(val > 0) {
14 int iarr[val];
15 for(int i = 0; i < val; i++) {
16 iarr[i] = i;
17 }
18 for(int i = 0; i < val; i++) {
19 printf("%d\n", iarr[i]);
20 }
21 }
22 return EXIT_SUCCESS;
23 }

Listing 9.3     »vla_arrays.c« demonstriert die Verwendung von Arrays variabler Länge.

In Zeile (14) sehen Sie die Definition eines int-Arrays, dessen Elementanzahl beim Start des Programms noch nicht bekannt ist und erst vom Anwender bestimmt wird. Dass dies tatsächlich funktioniert, können Sie in Zeile (16) erkennen. Dort wurde den einzelnen Elementen ein Wert zugewiesen. In Zeile (19) werden die Werte der einzelnen Elemente ausgegeben. Damit dieses Beispiel überhaupt funktioniert, ist es wichtig, dass die Definition in Zeile (14) in einem Anweisungsblock zwischen den Zeilen (13) bis (21) steht. Nur innerhalb dieses Bereichs ist das VLA-Array iarr gültig.

In der Praxis spricht somit nichts dagegen, die variable Länge von Arrays, wenn Ihr Compiler dies unterstützt, auch in Funktionen zu verwenden. Hier ist ein Beispiel, wie Sie dies umsetzen können:

void varArray( int v ) {
float fvarr[v];
...
}
...
// Funktionsaufruf
varArray(25);

9.1.7    Arrays mit scanf einlesen

Das Einlesen von einzelnen Array-Werten funktioniert im Grunde genommen genauso wie mit gewöhnlichen Variablen. Sie haben allerdings neben dem Adressoperator noch den Indizierungsoperator []. In den eckigen Klammern geben Sie den Wert für den Index an, mit dem Sie das eingelesene Element versehen wollen. In Listing 9.4 sehen Sie ein Beispiel hierzu:

00  // Kapitel9/array_eingabe.c
01 #include <stdio.h>
02 #include <stdlib.h>
03 #define MAX 3

04 int main(void) {
05 double dval[MAX];
06 for(int i = 0; i < MAX; i++) {
07 printf("%d. double-Wert: ", i+1);
08 if (scanf("%lf", &dval[i]) != 1) {
09 printf("Fehler bei der Eingabe ...\n");
10 return EXIT_FAILURE;
11 }
12 }
13 printf("Sie gaben ein: ");
14 for(int i = 0; i < MAX; i++) {
15 printf("%.2lf ", dval[i]);
16 }
17 printf("\n");
18 return EXIT_SUCCESS;
19 }

Listing 9.4     »array_eingabe.c« liest sämtliche Array-Elemente von »dval« per »scanf()« ein.

Abgesehen von Zeile (08), in der mithilfe des Adressoperators, des Indizierungsoperators und des entsprechenden Indexwertes MAX-Werte in das Array eingelesen werden, enthält das Listing nichts Unbekanntes. Das Programm sieht bei der Ausführung beispielsweise so aus:

1. double-Wert: 3.1
2. double-Wert: 3
3. double-Wert: 0.55
Sie gaben ein: 3.10 3.00 0.55

9.1.8    Arrays an Funktionen übergeben

An dieser Stelle kommen wir nicht umhin, auf Abschnitt 10.4, »Zeiger als Funktionsparameter«, vorzugreifen, weil Arrays an Funktionen nicht wie andere Variablen als Kopie übergeben werden können, sondern als Adresse übergeben werden müssen. Somit übergeben Sie hier kein komplettes Element bzw. das komplette Array als Kopie an die Funktion, sondern nur die Anfangsadresse dieses Arrays und dessen Länge in einem weiteren Parameter.

Deshalb wirken sich Änderungen an diesen Werten auch direkt auf das beim Aufruf übergebene Array aus. Dies wird als Call by Reference bezeichnet. Sie greifen dann quasi direkt auf die Adressen der einzelnen Array-Elemente des Aufrufers zu. An dieser Stelle sind Sie vielleicht noch nicht so gut mit Zeigern vertraut. Überspringen Sie in diesem Fall den Rest dieses Kapitels, lesen Sie zuerst Kapitel 10, »Zeiger (Pointer)«, und kehren Sie anschließend hierher zurück.

[»]  Arrays werden sequenziell gespeichert

Arrays sind, wie gesagt, keine Reihen und auch (zumindest in mathematischer Hinsicht) keine Vektoren. Trotzdem müssen sie irgendwie im Speicher abgelegt werden, und dies erfolgt sequenziell. Wenn Sie also die Anfangsadresse von einem Array an eine Funktion übergeben, können Sie sich darauf verlassen, dass die einzelnen Array-Elemente im Speicher sequenziell abgelegt sind bzw. sein müssen. Deshalb genügt es, die Anfangsadresse und die Länge eines Arrays an eine Funktion zu übergeben, um auf das gesamte Array zugreifen zu können. Die Funktion muss natürlich genau wissen, wie Ihr Array aufgebaut ist und wie viele Dimensionen es hat.

Sie übergeben ein Array an eine Funktion, indem Sie zusätzlich zur Anfangsadresse des Arrays einen formalen Parameter erstellen. Dort können (und müssen) Sie die Anzahl der Elemente des Arrays mit an die Funktion übergeben).

Listing 9.5 bietet hierzu ein einfaches Beispiel:

00  // Kapitel9/funcarray.c
01 #include <stdio.h>
02 #include <stdlib.h>
03 #define MAX 3

04 void readArray( int arr[], int n ) {
05 for(int i=0; i < n; i++) {
06 printf("[%d] = %d\n", i, arr[i]);
07 }
08 }

09 void initArray( int arr[], int n) {
10 for(int i=0; i < n; i++) {
11 arr[i] = i+i;
12 }
13 }

14 int main(void) {
15 int iArr[MAX];
16 initArray( iArr, MAX );
17 readArray( &iArr[0], MAX );
18 return EXIT_SUCCESS;
19 }

Listing 9.5     »funcarray.c« zeigt, wie Sie ein Array an eine Funktion übergeben.

In Zeile (16) übergeben Sie die Adresse des in Zeile (15) definierten Arrays und die Anzahl der Elemente an die Funktion initArray(). Diese wurde in den Zeilen (09) bis (13) definiert. Innerhalb der Funktion initialisieren Sie dann die einzelnen Elemente des Arrays mit Werten. In Zeile (17) des Programms übergeben Sie die Anfangsadresse des Arrays mit der Anzahl der Elemente an die Funktion readArray() (Zeilen (04) bis (08)). Dort werden die einzelnen Elemente des Arrays ausgegeben. Beide Schreibweisen in den Zeilen (16) und (17) sind übrigens gleichwertig. In Zeile (17) übergeben Sie die Adresse des ersten Elements aber direkt an die Funktion.

Es wäre theoretisch auch möglich, die Adresse des zweiten Elements im Array mit

readArray(&iArr[1], MAX-1); 

an die Funktion zu übergeben. Allerdings müssen Sie dann auch die Anzahl der Elemente entsprechend anpassen, um einen Überlauf zu vermeiden. Sie sehen hier, dass die Übergabe eines Arrays an eine Funktion Gefahren birgt, weil sich die Funktion darauf verlassen muss, dass die von Ihnen gelieferten Längenangaben korrekt sind.