9 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, kurz 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, werden 27 Werte hintereinander im Speicher abgelegt. Deshalb werden Arrays hin und wieder fälschlicherweise als Vektoren oder Reihen bezeichnet.
Aber Achtung! Ein Vektor ist beispielsweise auch ein mathematisches Konstrukt und in C kein Speicherobjekt. In anderen Sprachen gibt es Vektoren als Objekte, die sich aber von Arrays unterscheiden. Eine Reihe (auch als Folge bezeichnet) ist dagegen 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 ist es sinnvoller, Arrays mit einer Matrize (oft auch Matrix genannt) gleichzusetzen. 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 und 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, müssen Sie dies wie folgt tun:
int Wuerfel[3][3][3]; // 3 Dimensionen mit jeweils 3 Werten
Array mit variabler Länge (VLA)
Im 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.
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 darauf zugreifen
Um einzelnen Array-Elementen einen Wert zu übergeben oder Werte daraus 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.
Hierzu folgt 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 }
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) wurde vom Indexwert 3 Gebrauch gemacht. 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 Wert n-1 (n ist die Arraygröße). In C gibt es keine Möglichkeit, um Arrays mit dem Index 1 anfangen zu lassen, wie es in einigen Programmiersprachen möglich ist.
Unser Array iArray mit drei Elementen vom Datentyp int aus dem Beispiel arraydefinition.c können Sie sich 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]);
würden Sie auf einen nicht geschützten und reservierten Speicherbereich zugreifen. Bestenfalls stürzt das Programm gleich mit einer Schutzverletzung (segmentation fault) oder Zugriffsverletzung (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.
Array-Überlauf überprüfen
Auf vielen Systemen gibt es eine Compiler- bzw. eine 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. Nur 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 sind ziemlich trivial. Häufig werden Sie Werte von Arrays in Schleifen übergeben oder auslesen. Hierbei kann es schnell mal zu einen Über- bzw. Unterlauf kommen, wenn Sie nicht aufpassen. Ein einfaches und typisches Beispiel dazu wäre:
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 }
Im dem letzten Programm 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. Vergessen Sie z. B., dass das erste Element eines Arrays mit dem Index 0 beginnt, machen Sie unter Umständen den folgenden Fehler:
Durch die Verwendung des <=-Operators statt des <-Operators werden jetzt 11 anstelle von 10 Array-Elemente 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 hier ist das Verhalten des Programms nicht vorhersehbar, was zu schwer auffindbaren Fehlern führen kann. Deshalb 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 (vor allem kleine Arrays) können Sie bereits bei der Definition mit einer Initialisierungsliste initialisieren. Hierbei wird bei der Definition eine Liste von Werten in geschweiften Klammern, getrennt durch Kommata, angegeben. 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:
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, 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. Es folgen nun einige Beispiele 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 der Definition 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
Mit dem C99-Standard wurde auch die Möglichkeit eingeführt, ein bestimmtes Element bei der Definition zu initialisieren. Hierzu müssen Sie in der Initialisierungsliste lediglich das gewünschte Element in eckigen Klammern angeben. Hier ein Beispiel:
#define MAX 5
int iArray[MAX] = { 123, 456, [MAX-1] = 789 };
Es wurden die ersten beiden Elemente initialisiert; anschließend wurde dem letzten Wert 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 Array 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:
#define RGB 3
// 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) – optional seit C11
Mit dem C99-Standard wurde ebenfalls eingeführt, dass bei der Definition die Anzahl der Elemente kein konstanter Ausdruck mehr sein muss. Seit dem C11-Standard ist die VLA-Unterstützung (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 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.
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. Hierzu 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 }
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 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 []. Zwischen den eckigen Klammern geben Sie den Wert für den Index an, mit dem Sie das eingelesene Element versehen wollen. Es folgt 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 }
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 von Kapitel 9, lesen zuerst Kapitel 10, »Zeiger (Pointer)«, und kehren anschließend zu dieser Stelle 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).
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 }
In Zeile (16) übergeben Sie die Adresse des in Zeile (15) definierten Arrays und die Anzahl der Elemente an die Funktion initArray(). Diese ist 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 das Übergeben eines Arrays an eine Funktion Gefahren birgt, weil sich diese darauf verlassen muss, dass die von Ihnen übergebenen Längenangaben korrekt sind.