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).
Noch keine Stimmen abgegeben
Noch keine Kommentare

Einordnung des Artikels

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:

  1. Der Datentyp der Komponenten des Feldes wird vereinbart (hier int ).
  2. Der Name des Feldes wird festgelegt (hier arr ).
  3. Die Anzahl der Komponenten wird festgelegt (hier 5 in den eckigen Klammern).

Abbildung 1: Versuch einer graphischen Darstellung der Deklaration eines Feldes. AbhĂ€ngig von der LĂ€nge und des Datentypes des Feldes wird eine Anzahl von Speicherzellen geeigneter GrĂ¶ĂŸe reserviert. Angesprochen werden die Speicherzellen ĂŒber den Namen des Feldes (mehr dazu bei Initialisierung von Feldern).Abbildung 1: Versuch einer graphischen Darstellung der Deklaration eines Feldes. AbhĂ€ngig von der LĂ€nge und des Datentypes des Feldes wird eine Anzahl von Speicherzellen geeigneter GrĂ¶ĂŸe reserviert. Angesprochen werden die Speicherzellen ĂŒber den Namen des Feldes (mehr dazu bei Initialisierung von Feldern).

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.

Abbildung 2: Graphischen Darstellung der Initialisierung eines Feldes.Abbildung 2: Graphischen Darstellung der Initialisierung eines Feldes.

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:

  1. Das Feld wird wie ĂŒblich deklariert (Zeile 1 und 3).
  2. Wie in der Schleife aus dem letzten Listing kann der Nutzer die Werte eingeben.
  3. 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).
  4. 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.

Abbildung 3: Oben: Deklaration einer Matrix mit 2 Zeilen und 3 Spalten. Intern wird die Matrix wie ein eindimensionales Feld abgespeichert; ĂŒber die deklarierten Zeilen- und Spalten-Anzahlen kann nach der Initialisierung der Zugriff auf die Feld-Komponenten ĂŒber die Matrizen-Indizes realisiert werden. Unten: Initialisierung der Matrix mit einer verschachtelten Initialisierungsliste.Abbildung 3: Oben: Deklaration einer Matrix mit 2 Zeilen und 3 Spalten. Intern wird die Matrix wie ein eindimensionales Feld abgespeichert; ĂŒber die deklarierten Zeilen- und Spalten-Anzahlen kann nach der Initialisierung der Zugriff auf die Feld-Komponenten ĂŒber die Matrizen-Indizes realisiert werden. Unten: Initialisierung der Matrix mit einer verschachtelten Initialisierungsliste.

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.