Felder (arrays) in C
In einem Feld werden mehrer Komponenten von gleichem Datentyp zu einem Objekt zusammengefasst. Die Anzahl der Komponenten muss bei der Deklaration angegeben werden und darf sich während der Laufzeit des Programmes nicht ändern. Der häufigste Fehler beim Umgang mit Feldern besteht im Zugriff auf Komponenten jenseits des deklarierten Bereichs, was zu unbestimmtem Verhalten des Programmes führen kann. Die unterschiedlichen Möglichkeiten zur Initialisierung eines Feldes werden vorgestellt. Felder können in beliebig vielen Dimensionen angelegt werden; besprochen werden hier nur eindimensionale Felder (Vektoren) und zweidimensionale Felder (Matrizen).
- Einordnung des Artikels
- Einführung
- Deklaration und Initialisierung von Feldern (arrays)
- Deklaration von Feldern
- Initialisierung von Feldern
- Initialisierung von Feldern durch Konsolen-Eingaben
- Zugriff auf die Komponenten eines Feldes
- Der häufigste Fehler: Zugriff auf nicht zulässige Komponenten
- Zweidimensionale Felder
- Überblick
- Deklaration eines zweidimensionalen Feldes
- Initialisierung eines zweidimensionalen Feldes und Zugriff auf die Komponenten
Einordnung des Artikels
- Einführung in die Informatik
- Einführung in die Programmiersprache C und in C-Zeiger
- Felder (arrays) in C
- Einführung in die Programmiersprache C und in C-Zeiger
Einfache Aufgaben zu den hier behandelten Konzepten finden sich in Programmier-Aufgaben in C: erste Schritte mit Feldern und Zeigern.
Einführung
Werden mehrere Objekte von identischem Datentyp (wie etwa int oder double) zu einem Objekt zusammengefasst, spricht man von einem Feld (array). Die Elemente des Feldes werden meist wie bei einem Vektor als Komponenten bezeichnet.
Einfacher Umgang mit Feldern, also die Deklaration, Initialisierung und der Zugriff auf die Komponenten, können auch ohne die Kenntnis von Zeigern realisiert werden. Sobald anspruchsvollere Aufgaben mit Feldern erledigt werden, wie etwa die Übergabe eines Feldes an eine Funktion, oder das Erzeugen eines Feldes durch eine Funktion, kommt man ohne die Kenntnis des Zusammenhangs zwischen Feldern und Zeigern nicht mehr aus.
Deklaration und Initialisierung von Feldern (arrays)
Deklaration von Feldern
Ein Feld fasst mehrere Größen von identischem Datentyp zu einem Objekt zusammen. Die Komponenten sind dann numeriert und man kann leicht über den Index auf sie zugreifen. Wird ein Feld deklariert, muss bereits bekannt sein, wie viele Komponenten das Feld besitzt.
Ein Feld, das dynamisch seine Größe ändert, ist nur mit einigen Verrenkungen zu realisieren. So ist etwa die Aufgabe, mit Hilfe eines Zufallsgenerators das Zufallsexperiment "so lange Würfeln bis die erste 6 erscheint" zu simulieren, vorerst nicht lösbar, da man vor dem Würfeln nicht weiß, welche Größe das Feld besitzen wird. In solchen Fällen behilft man sich damit, das Feld "groß genug" zu wählen oder man arbeitet sich in die sogenannte Speicher-Allokation ein.
Die Deklaration eines Feldes sieht typischerweise wie folgt aus:
int arr [5];
Hier wird ein Feld namens arr der Länge 5 deklariert, aber noch nicht initialisiert, das heißt die Komponenten sind noch nicht mit Werten belegt. Genauer gesagt wird hier der Speicherplatz für die 5 Komponenten reserviert und die Komponenten sind mit denjenigen Werten belegt, die sich zum Zeitpunkt der Deklaration gerade in den Speicherzellen befinden.
Es ist daher ratsam, ein Feld sofort nach (oder zusammen mit) der Deklaration zu initialisieren, also die Komponenten mit definierten Werten zu belegen. Wird ein Feld nicht sofort initialisiert, sollte man besser im Kommentar den Grund angeben, warum es erst später initialisiert wird. Mehr zur Initialisierung folgt im nächsten Unterabschnitt.
Eine Deklaration besteht aus drei Elementen:
- Der Datentyp der Komponenten des Feldes wird vereinbart (hier
int
). - Der Name des Feldes wird festgelegt (hier
arr
). - Die Anzahl der Komponenten wird festgelegt (hier 5 in den eckigen Klammern).
Initialisierung von Feldern
Die Initialisierung eines Feldes kann auf verschiedene Weisen erfolgen, sie werden im Folgenden gezeigt. Es gibt auch einige Varianten, die zu Compiler-Fehlern führen, obwohl man vielleicht erwartet, dass sie möglich sind; sie werden zwar gezeigt, sind aber auskommentiert. Es wird empfohlen, sie zu testen und auf die Fehlermeldung des Compilers zu achten.
Sowohl bei der Initialisierung eines Feldes als auch beim Zugriff auf die Komponenten ist darauf zu achten, dass die Indizierung mit 0 beginnt; ein Feld mit 5 Komponenten besitzt also die Indizes 0, 1, 2, 3, 4. Wenn später der Zusammenhang zwischen Feldern und Zeigern hergestellt wird, lässt sich diese Vereinbarung leichter nachvollziehen.
# define LENGTH_ARRAY 5
int arr1 [5] = {2, 4, 6, 8, 10};
// 2 4 6 8 10
int arr2 [] = {2, 4, 6, 8, 10};
// 2 4 6 8 10
int arr3 [LENGTH_ARRAY];
for (int i = 0; i < LENGTH_ARRAY; ++i) {
arr3[i] = 2 * i;
}
// 0 2 4 6 8
// int arr4 [5] = arr3; // Compiler-Fehler: rechts darf kein Feld stehen
int arr5 [5];
//arr5 = {2, 4, 6, 8, 10}; // Compiler-Fehler: Zuweisung erfolgt zu spät
Zur Erklärung:
Zeile 1: Es wird die symbolische Konstante LENGTH_ARRAY
definiert. Der Präprozessor sorgt dafür, dass jede Verwendung von LENGTH_ARRAY
durch die Zahl 5 ersetzt wird; dies geschieht bereits, bevor das Programm kompiliert wird. Warum man dies macht, wird erklärt, wenn die Konstante LENGTH_ARRAY
verwendet wird (siehe Zeile 9 und 10).
Zeile 3: Das Feld arr1 wird deklariert und sofort initialisiert. Die Initialisierung erfolgt in der sogenannten Initialisierungsliste, das heißt die Werte der Komponenten werden in geschweiften Klammern durch Kommas getrennt angegeben.
Zeile 6: Beim Einsatz der Initialisierungsliste ist es nicht zwingend erforderlich, die Anzahl der Komponenten in den eckigen Klammern anzugeben. Der Compiler erkennt sie an der Liste auf der rechten Seite der Zuweisung. Wurde in Zeile 3 die Länge des Feldes explizit angegeben, wird in Zeile 6 die Länge des Feldes implizit bestimmt.
Zeile 9: Deklaration des Feldes arr3 mit Hilfe der symbolischen Konstante LENGTH_ARRAY
. Da in Zeile 1 die Konstante gleich 5 gesetzt wurde, ist für den Compiler diese Zeile nicht von int arr3 [5];
zu unterscheiden. Den Vorteil der Verwendung der Konstante erkennt man daran, wie hier das Feld initialisiert wird.
Zeile 10 bis 12: Initialisierung des Feldes arr3 in einer for-Schleife. Als Variable für die Indizes wird i vereinbart, sie läuft von 0 bis 4. In jedem Schleifendurchlauf wird eine Komponente des Feldes gesetzt.
An der Initialisierung von arr3 erkennt man, warum die Verwendung der symbolischen Konstante vorteilhaft sein kann: Möchte man das Programm später mit einer anderen Länge des Feldes verwenden, muss man nur an einer Stelle in den Quelltext eingreifen, nämlich in Zeile 1, und LENGTH_ARRAY
abändern. Wie oben erklärt, eine dynamische Änderung der Länge des Feldes ist deutlich komplizierter.
Zeile 15: Bei fundamentalen Datentypen ist es nicht ungewöhnlich, Zuweisungen der Art x = y
vorzunehemen (wenn x bereits deklariert wurde und y sogar initialisiert). Bei Feldern ist dies nicht möglich. Der Grund ist leicht einzusehen, wenn der Zusammenhang zwischen Feldern und Zeigern hergestellt wird.
Zeile 17 und 18: Damit sollte auch klar sein, dass die Initialisierung mit einer Initialisierungsliste fehlerhaft ist, wenn sie von der Deklaration abgetrennt wird. Es spielt keine Rolle, ob in der Zuweisung rechts in Zeile 16 eine Variable oder eine Initialisierungsliste steht.
In Abbildung 2 wird versucht, die Initialisierung eines Feldes graphisch darzustellen: Das Feld a ist vom Datentyp int und besitzt 5 Komponenten. Bei der Deklaration werden 5 geeignete Speicherzellen reserviert, die dann mit den Werten des Feldes gefüllt werden. Der Zugriff auf die Komponenten erfolgt mit Hilfe des Operators [ ]
; zu beachten ist dabei, dass die Zählung der Komponenten immer bei 0 beginnt.
Aufgabe:
Testen Sie das Verhalten, wenn eine Initialisierung wie in Zeile 3 vorgenommen wird, aber die Länge des Feldes (in eckigen Klammern) und die Länge der Initialisierungsliste nicht übereinstimmen. (Es gibt zwei Fälle, je nachdem welche Länge größer ist.)
Initialisierung von Feldern durch Konsolen-Eingaben
Eine spezielle Initialisierung eines Feldes soll separat besprochen werden: Die Eingabe der Werte über die Konsole. Dies kann leicht zu einem Konflikt führen, nämlich dann wenn der Nutzer des Programmes die zuvor vereinbarte Länge des Feldes nicht kennt und er versucht, weitere Werte einzugeben.
Im folgenden Programm wird dieser Konflikt vermieden, indem die Eingabe innerhalb einer Schleife erfolgt, die mit der gewünschten Feldlänge konfiguriert wird:
# define LENGTH_ARRAY 5
int a[LENGTH_ARRAY];
for (int i = 0; i < LENGTH_ARRAY; ++i){
puts("Geben Sie eine Komponente ein: ");
scanf ("%d", & a[i]);
}
Zeile 1: Es wird wieder die Feldlänge als Konstante LENGTH_ARRAY
definiert.
Zeile 3: Mit Hilfe der Konstante wird das Feld a deklariert, aber noch nicht initialisiert.
Zeile 5 bis 8: Die for-Schleife ist so konfiguriert, dass sie die Indizes von a durchläuft. Mit dieser Konstruktion ist man somit auf der sicheren Seite, um alle Indizes abzuarbeiten und keine unerlaubten Indizes aufzurufen. In Zeile 6 wird der Nutzer aufgefordert, die nächste Komponente des Feldes einzugeben. Die Funktion scanf() besitzt 2 Argumente:
- im ersten Argument liest scanf() die Konsoleneingabe und
- setzt damit (durch das zweite Argument) den Wert der i-ten Komponente von a; dazu muss dessen Adresse an scanf() übergeben werden, was mit Hilfe des Adressoperators geschieht.
Oben wurde bereits gesagt, wie man sich behelfen kann, wenn eigentlich ein Feld mit dynamisch veränderlicher Größe benötigt wird: Man setzt die Länge groß genug an; die restlichen Komponenten werden dann auf einen default-Wert gesetzt.
Das folgende Listing zeigt, wie man dies realisieren kann, wenn der Nutzer die Werte der Komponenten eingeben kann, aber im Voraus nicht bekannt ist, wie viele er eingeben wird:
- Das Feld wird wie üblich deklariert (Zeile 1 und 3).
- Wie in der Schleife aus dem letzten Listing kann der Nutzer die Werte eingeben.
- Der Unterschied besteht jetzt darin, dass eine Zahl vereinbart wird, die den Abbruch der Eingabe signalisiert (hier -1). Diese Zahl muss natürlich so gewählt werden, dass die in dem Feld nicht vorkommen kann (wie etwa die -1 beim Würfeln).
- Die Komponenten, die vom Nutzer nicht gesetzt werden, werden in einer anschließenden Schleife auf einen geeigneten default-Wert gesetzt (hier 0).
# define L 5
int a[L];
int i;
for (i = 0; i < L; ++i){
puts("Geben Sie eine Komponente ein: ");
puts("(Abbruch durch Eingabe von -1):\n");
scanf ("%d", & a[i]);
if (a[i] < 0 ) break;
}
for (int j = i; j < L; ++j){
a[j] = 0;
}
Man beachte, wie wichtig es ist, die Zählvariable i der ersten Schleife bereits vor der Schleife zu deklarieren (Zeile 5); in den früheren Beispielen wurde die Zählvariable erst innerhalb der for-Anweisung deklariert. Denn man benötigt nach der i-Schleife den aktuellen Wert von i, bei dem die break-Anweisung aufgerufen wurde, um die die j-Schleife mit dem richtigen i-Wert fortzusetzen (Zeile 13).
Zugriff auf die Komponenten eines Feldes
Der einfachste Zugriff auf die Komponenten eines Feldes erfolgt über den Index, der in eckigen Klammern angegeben wird. Zu beachten ist dabei immer, dass bei einem Feld mit n Komponenten der Index die Werte 0, 1, ..., n - 1 annimmt.
Sollen sämtliche Werte eines Feldes ausgegeben oder weiterverarbeitet werden, iteriert man über den Index in einer Schleife wie sie oben zur Initialisierung verwendet wurde:
int a[] = {2, 4, 6, 8, 10};
for (int i = 0; i < L; ++i) {
printf("%d ", a[i]);
}
// 2 4 6 8 10
Der häufigste Fehler: Zugriff auf nicht zulässige Komponenten
Der häufigste Fehler, der im Umgang mit Feldern gemacht wird, ist der Zugriff auf Komponenten außerhalb des deklarierten Bereichs. Oft ist die Ursache, dass man man annimmt, ein Feld mit 5 Komponenten besitzt eine Komponente mit Index 5.
Es ist wichtig zu wissen, welche Konsequenzen es hat, wenn ein derartiger unerlaubter Zugriff stattfindet. Dazu wird im folgenden Listing versucht, in einem Feld mit 5 Komponenten eine 6. Komponente zu setzen und diese abzufragen.
# define L 5
int a [L];
for (int i = 0; i == L; ++i) {
a[i] = 2 * i;
}
for (int i = 0; i == L; ++i) {
printf("Komponente %d : %d \n", i, a[i]);
}
Man beachte, dass jetzt in Zeile 4 und 8 jeweils i == L;
steht, richtig wäre i < L
. Dadurch wird in beiden Schleifen auf die Komponente a[5]
zugegriffen, die es laut Deklaration von a nicht gibt. Der Compiler zeigt weder eine Warnung noch einen Fehler. Das Verhalten des Programmes ist dann unbestimmt: Wurden die entsprechenden Speicherzellen vorher durch andere Variable belegt, wird darauf zugegriffen. Falls nicht, wird das Programm nicht ausgeführt.
Es liegt daher stets in der Verantwortung des Programmierers, unerlaubte Zugriffe auszuschließen.
Zweidimensionale Felder
Überblick
Bisher wurden Felder wie Vektoren betrachtet: Sie besitzen einen Index und werden durch eine Liste initialisiert. In der Mathematik wird aber auch mit Matrizen, also zweidimensionalen Feldern, gearbeitet, wobei die Komponenten durch zwei Indizes charakterisiert werden. Und diese können zu n-dimensionalen Feldern verallgemeinert werden, für die n Indizes benötigt werden.
In C lassen sich Felder beliebiger Dimension realisieren; hier werden aber nur zweidimensionale Felder beschrieben. Wenn man allerdings die Deklaration und Initialisierung eines zweidimensionalen Feldes genauer betrachtet, erkennt man, inwiefern sie rekursiv aufgebaut ist, und für n-dimensionale Felder verallgemeinert werden kann.
Es wird hier auch nicht auf alle Spitzfindigkeiten eingegangen, die im Zusammenhang mit zweidimensionalen Felder zu beachten sind – die meisten wurden für eindimensionale Felder bereits erklärt.
Ebenso werden hier keine komplizierten Matrix-Operationen behandelt, wie Matrizen-Multiplikation, Gauß-Algorithmus zum Lösen von Gleichungssystemen oder Invertierung von Matrizen. Wer auf diesem Abstraktionsgrad arbeiten möchte, sollte mit C++ arbeiten und entsprechende Bibliotheken nachladen. Derartige Operationen selber zu programmieren ist viel zu aufwendig und fehleranfällig.
Stattdessen wird hier gezeigt:
- wie man zweidimensionale Felder deklariert,
- initialisiert und
- auf die Komponenten zugreifen kann.
Deklaration eines zweidimensionalen Feldes
Da jetzt zwei Indizes den Zugriff auf die Komponenten regeln, ist es nicht schwer zu erraten, wie ein zweidimensionales Feld deklariert wird:
int m [2] [3];
Der erste Index gibt die Zeile der Matrix an, der zweite Index die Spalte. Dies entspricht genau unserer Leserichtung: Man liest zuerst von links nach rechts und dann von oben nach unten.
Initialisierung eines zweidimensionalen Feldes und Zugriff auf die Komponenten
Für die Initialisierung gibt es zwei Möglichkeiten:
- Sie erfolgt mit einer Initialisierungsliste, die jetzt genauer gesagt eine Liste von Listen ist.
- Man schreibt eine verschachtelte Schleife, in der die Komponenten der Matrix einzeln gesetzt werden.
Die beiden Möglichkeiten werden im Folgenden gezeigt. Es wird zweimal die Matrix m erzeugt, die auf der Hauptdiagonale 1 stehen hat, ansonsten 0 (siehe auch Abbildung 3):
// 1 0 0
// 0 1 0
1. Initialisierungsliste:
int m[2][3] = {{1, 0, 0}, {0, 1, 0}};
Intern wird die Matrix wie ein eindimensionales Feld behandelt, siehe Abbildung 3 unten.
Man erkennt an der Initialisierung des zweidimensionalen Feldes die rekursive Struktur der Initialisierungsliste: Die Matrix m besitzt 2 Zeilen und 3 Spalten.
- Für die zwei Zeilen wird die äußere Initialisierungsliste verwendet (mit 2 Komponenten).
- Jeder Eintrag in der äußeren Liste ist wiederum eine Liste, nämlich das Feld, das in der entsprechenden Zeile stehen soll. Die inneren Listen müssen daher die Länge 3 haben.
Man kann sich daher leicht vorstellen, wie ein mehr-dimensionales Feld initialisiert wird.
Da die Matrix aus sehr vielen Nullen besteht, was in Anwendungen oft vorkommt, gibt es auch eine vereinfachte Version der Initialisierungsliste: Man schreibt die Zeilen nur bis zur letzten Komponente, die ungleich 0 ist. Die nachfolgenden Komponenten werden automatisch auf 0 gesetzt. (Man sieht hier wieder, wie wichtig die Deklaration ist: ohne die Kenntnis der Zeilen- und Spalten-Anzahlen könnte der Compiler die Zeilen nicht geeignet auffüllen.)
Identisch zur Initialisierung oben ist daher:
int m[2][3] = {{1}, {0, 1}};
2. Initialisierung in einer Doppel-Schleife:
int m[2][3] = {{1, 0, 0}, {0, 1, 0}};
for (int i = 0; i < 2; ++i) {
for (int j = 0; j < 3; ++j) {
if (i == j) { // Hauptdiagonale
m[i][j] = 1;
} else { // Elemente außerhalb der Hauptdiagonale
m[i][j] = 0;
}
}
}
Man beachte die Iterationen:
- die äußere Schleife (Index i) iteriert über die Zeilen,
- die innere Schleife (Index j) iteriert über die Spalten.
Der Programmieraufwand ist hier deutlich höher als bei der Initialisierungsliste; bei Matrizen, deren Komponenten aber erst während der Laufzeit des Programmes berechnet werden, wird man aber an dieser Art der Initialisierung nicht vorbeikommen.
Der Zugriff auf die Komponenten einer Matrix erfolgt dann über die Angabe beider Indizes (Zeile 5):
int m[2][3] = {{1, 0, 0}, {0, 1, 0}};
for (int i = 0; i < 2; ++i) {
for (int j = 0; j < 3; ++j) {
printf("%d ", m[i][j]);
}
}
// 1 0 0 0 1 0
Da sich die printf()-Anweisung im Inneren der beiden Schleifen befindet, werden alle Komponenten der Matrix in einer Zeile ausgegeben. Für eine zeilenweise Ausgabe müsste man in der äußeren Schleife den Zeilenumbruch erzwingen.