8.1. Felder: Variablen im Paket

Definition und Gebrauch von Feldern

Wie bereits erwähnt, sind Arrays (Felder) eine Zusammenfassung mehrerer Variablen gleichen Typs unter einem Namen. Jedes Element eines Arrays kann eindeutig über die Kombination von Name und Index angesprochen werden. Wie alle anderen Variablen müssen auch Felder definiert bzw. deklariert werden. Dies geschieht in der Form:


Datentyp Name[Anzahl der Elemente];

Bei der Vergabe des Namens für ein Array gelten die gleichen Konventionen wie bei den Variablennamen; die Größe des Feldes muß durch eine integer-Konstante bekanntgegeben werden. Folgende Beispiele stehen für gültige Definitionen von Feldern:


int iWoche[52];    /* Feld mit 52 Elementen vom Typ int */
char szMsg[100]; /* Zeichenkette mit maximal 100 Zeichen */
float fBetrag[10]; /* 10 Elemente vom Typ float */

Die Elemente eines Arrays belegen in C einen zusammenhängenden Speicherbereich, d.h., sie befinden sich der Reihe nach an aufeinanderfolgenden Adressen. Mit Hilfe des Beispiels arraysize.c können wir diese Tatsache ohne weiteres belegen. Sie ist von Bedeutung, wenn wir später mit Hilfe von Pointern auf einzelne Elemente von Feldern zugreifen wollen.


/*
 * arraysize.c
 * gibt die Größe des Feldes iArray in Byte
 * und die Anfangsadressen des ersten und letztes
 * Elements aus.
 */
#include <stdio.h>

int main(void){

int iArray[10];    /* Array aus 10 int-Werten */


printf("iArray belegt auf unserem System %d Byte\n",
	 sizeof(iArray));

/* Der Adreßoperator '&' funktioniert auch mit
 * Elementen eines Arrays
 */
printf("Adresse des ersten Elements: %lu\n"

	"Adresse des letzten Elements: %lu\n",
	&iArray[0], &iArray[9]);

 return 0;
}

Die Ausgabe unseres Beispielprogramms zeigt, daß zwischen den Anfangsadressen des ersten und des letzten Elements 36 Byte liegen; zählt man die 4 Byte für das letzte Element hinzu, ergibt sich eine Ausdehnung des gesamten Feldes über 40 Byte. Den gleichen Wert konnten wir vorher mit Hilfe des sizeof-Operators ermitteln. Diese Übereinstimmung belegt, daß alle Elemente von iArray im Speicher unmittelbar aufeinander folgen.

arraysize liefert die folgende Ausgabe:


iArray belegt auf unserem System 40 Byte
Adresse des ersten Elements: 245628
Adresse des letzten Elements: 245664

Wie schon aus arraysize.c ersichtlich wurde, besitzt das erste Element eines Feldes den Index 0. Daraus leitet sich ab, daß das letzte Element den Indexwert Anzahl der Elemente — 1 besitzt. Bespielsweise stellen nach der Definition


int iWoche[52];

iWoche[0] das erste und iWoche[51] das letzte Element des Feldes dar. Der Versatz zwischen der Position eines Elements und dessen Index-Wert ist häufige Quelle tückischer Programmierfehler. C überprüft nämlich nicht, ob über das Ende eines Feldes hinausgeschrieben wird: Auf diese Weise können Adreßbereiche belegt werden, die für andere Daten reserviert sind.


/* 
 * array.c
 * definiert ein Array iSquare mit 10 Elementen vom
 * Typ int und weist jedem Element das Quadrat seines
 * Index-Werts zu.
 */
#include <stdio.h>

int main(void){

int iSquare[10];
/* Array mit 10 Elementen vom Typ int */
unsigned short usCount;

/* -----------------> Achtung <-----------------
 * for( usCount = 0; usCount <= 10; usCount++)
 * würde ohne Warnung über das Ende von iSquare
 * hinausschreiben!
 */
 for( usCount = 0; usCount <= 9; usCount++)
	iSquare[usCount] = usCount * usCount;


 printf("Werte der Feldelemente von iSquare:\n");

 for( usCount = 0; usCount <= 9; usCount++)
	printf("iSquare[%d] hat den Wert: %d\n",
		usCount, iSquare[usCount]);

 return 0;
 }

Sollte sich in array.c herausstellen, daß die Anzahl von 10 Elementen für iSquare nicht angemessen ist, dann müßten Sie nachträglich sowohl die Definition des Arrays als auch sämtliche Abbruchbedingungen in for-Schleifen verändern, mit deren Hilfe Sie sich durch das Array bewegen. In einem größeren Programm verursachte dies einigen Aufwand und wäre zudem fehlerträchtig. Um eventuelle Änderungen der Array-Größe zu erleichtern, hat es sich eingebürgert, bei der Definition von Arrays symbolische Konstanten zu verwenden. Eine solche läßt sich mit der Preprozessor-Anweisung #define vereinbaren (näheres dazu im Kapitel 12). Die Verwendung eines symbolischen Namens bietet außerdem den Vorteil, daß sie die Überschreitung von Array-Grenzen vermeiden hilft. array1.c demonstriert diese Vorgangsweise:


/* array1.c
 * verwendet symbolische Konstante bei der
 * Definition von iSquare
 */
#include <stdio.h>

#define SIZE 10
/* Die symbolische Konstante SIZE wird
 * vom Preprozessor überall durch 10 ersetzt!
 */
int main(void){

int iSquare[SIZE];
unsigned short usCount;

 for( usCount = 0; usCount < SIZE; usCount++)
 /* hier darf keinesfalls der <= Operator ver-
  * wendet werden!
  */
	iSquare[usCount] = usCount * usCount;

 printf("Werte der Feldelemente von iSquare:\n");

 for( usCount = 0; usCount < SIZE; usCount++)
	printf("iSquare[%d] hat den Wert: %d\n",
		usCount, iSquare[usCount]);

 return 0;
 }

Diese Version ist wesentlich leichter verständlich, da nicht über die Bedeutung einzelner Zahlen gerätselt werden muß. Besonders praktisch ist dabei, daß bei einer nachträglichen Änderung der Array-Größe nur die #define-Anweisung angepaßt werden muß! Die Verwendung von SIZE innerhalb der Schleifenanweisung erfordert allerdings den Operator <, da ja der größte Index um 1 unter der Anzahl der Elemente liegt. Die Kombination aus symbolischer Konstante und konsequenter Verwendung des Operator < sollten jedenfalls das Risiko falscher Indizierung erheblich reduzieren.

Während bei der Definition von Arrays die Anzahl der Elemente durch eine integer-Konstante festgelegt werden muß, können Sie beim Zugriff auf einzelne Elemente beliebige integer-Ausdrücke verwenden, um den Index zu berechnen. Unser Beispiel array.c nimmt den Wert des Schleifenzählers usCount, um den Index des aktuellen Elements zu bestimmen. Bei der Verwendung einer while-Schleife können wir zusätzlich die Zählervariable innerhalb der eckigen Klammern erhöhen:


.
.
#define SIZE 10
 usCount = 0;
 while( usCount < SIZE )
	printf("iSquare[%d] hat den Wert: %d\n",
		usCount, iSquare[usCount++]);
.
.

Beachten Sie, daß es sich bei den eckigen Klammern um Operatoren handelt. Ihre Priorität liegt über jener von arithmetischen Operatoren. Der Ausdruck innerhalb der eckigen Klammern wird daher völlig ausgewertet, bevor C ein Feldelement für irgendwelche Operationen heranzieht. Eine Ausnahme bildet jedoch der Inkrement- bzw. Dekrementoperator in Postfixposition: Im obigen Fragment wird der Wert der Variablen usCount erst nach dem Ausdrucken des jeweilgen Array-Elements erhöht.

Mehrdimensionale Felder

Neben eindimensionalen Feldern, die man sich als Kette aneinandergereihter Variablen gleichen Typs vorstellen kann, bietet C auch mehrdimensionale Felder. Zweidimensionale Arrays kann man sich als Tabellen vorstellen, wobei der erste Index für die Zeile, der zweite für die Spalte steht. Ein zweidimensionales Feld wird z.B. folgendermaßen definiert:


int iTabelle[10][10];

Die Variable iTabelle[4][5] würde gemäß unseres Vergleichs das Element in der 5. Zeile und 6. Spalte repräsentieren.

Das folgende Beispiel verwendet ein dreidimensionales Array, um die Tagestemperaturen von maximal 10 Jahren zu speichern. Wie uns das Programm selber schon mitteilt (Array fTemperatur belegt 16640 Byte), sind mehrdimensionale Felder speicherhungrig. Während ein eindimensionales Feld Anzahl der Elemente * sizeof(Datentyp) Byte belegt, multipliziert sich dieser Wert bei mehrdimensionalen Feldern zusätzlich mit der Anzahl der Elemente jedes weiteren Indexes. Unser obiges Feld Tabelle belegt daher 10 * 10 * sizeof(int) Byte. Aufgrund des Speicherbedarfs von fTempertatur ist array_3.c ein typisches Programm der 90er Jahre. Wollten wir einen Bereich von 100 Jahren abdecken, dann benötigen 32 * 13 * 100 Variablen vom Typ float sogar mehr als 64 KByte Speicher!


/* array_3.c
 * Beispiel für die Verwendung eines
 * 3dimensionalen Arrays

#include <stdio.h>

int main(void){

int iTageProMonat;
/* 1.Index für Tag, 2. für Monat, 3. für Jahr */
float fTemperatur[32][13][10], fSumme = 0;
int iCount, iEingabeMonat, iEingabeJahr;

 printf("Array fTemperatur belegt %d Byte\n",
	 sizeof(fTemperatur));
 printf("Für welchen Monat sollen"
	" Werte eingeben werden?(mm/j)\n");
 scanf("%2d%*c%1d", &iEingabeMonat, &iEingabeJahr);
 /* Tastaturpuffer löschen */
 fflush(stdin);

/* eruieren, wie viele Tage unser Monat hat */
	 switch(iEingabeMonat){
		case 1:
		case 3:
		case 5:
		case 7:
		case 8:
		case 10:
		case 12: iTageProMonat = 31;
			break;
	/* Beim Februar auf Schaltjahr achten. Da scanf() nur
	 * die letzte Stelle der Jahreszahl einliest, 1990 addieren
	 */
		case 2: (iEingabeJahr + 1990) % 4 == 0 &&
			(iEingabeJahr + 1990) % 100 != 0 ||
			(iEingabeJahr + 1990) % 400 == 0  ?
			(iTageProMonat = 28) : (iTageProMonat=29);
			break;
		 default: iTageProMonat = 30;
			break;
		 }

 printf("Bitte Werte für betreffenden Monat eingeben!\n");

 for(iCount = 1; iCount <= iTageProMonat; iCount++){

	 printf("\n%d.%d.199%d ",
		iCount, iEingabeMonat, iEingabeJahr);
	 scanf("%f",
       &fTemperatur[iCount][iEingabeMonat][iEingabeJahr]);
 	 fSumme +=
       fTemperatur[iCount][iEingabeMonat][iEingabeJahr];
	 }

 printf("\nDurchschnitt für %d.199%d.: %f Grad",
	iEingabeMonat, iEingabeJahr, fSumme / iTageProMonat);

 return 0;
}

Das Programm beginnt damit, daß es den Benutzer um Eingabe eines Monats und eines Jahres auffordert. Bei der Jahreszahl geht es stillschweigend von einem Wert zwischen 1990 und 1999 aus und liest aufgrund der begrenzten Kapazität unseres Arrays mit scanf() nur die die letzte Stelle ein. Statt eines leeren getchar() verwenden wir hier die von ANSI definierte Bibliotheksfunktion fflush(), um den Puffer der Standardeingabe zu löschen. Anschließend ist es Aufgabe der switch/case-Konstruktion, die Anzahl der Tage für den fraglichen Monat zu ermitteln. Dabei stellt der bedingte Ausdruck unter case 2 sicher, daß für den Februar ein eventuell vorliegendes Schaltjahr berücksichtigt wird. Innerhalb der darauffolgenden for-Schleife liest scanf() die einzelnen Tageswerte ein; der erste Wert kommt in die Variable iTemperatur[1][iEingabeMonat][iEingabeJahr]. Der Index für den Tag wird bei jedem Schleifendurchlauf hochgezählt (maximal bis iTemperatur[31][iEingabeMonat][iEingabeJahr]), die Werte für Monat und Jahr bleiben natürlich unverändert.

Auffällig beim Feld iTemperatur ist, daß die Indexe für Tag und Monat auf 32 bzw. 13 Elemente ausgelegt sind. Das erweist sich deswegen als sinnvoll, weil der auf 0 basierende Index in C beispielsweise das Datum 0.0.1995 nach sich zöge. Wenn Sie das Array um 1 größer dimensionieren als die maximale Anzahl der Elemente erfordert, dann kann das erste Element mit dem Index 0 unbenutzt bleiben; so ist es hier möglich, Tage und Monate in gewohnter Weise durchzunumerieren.

Neben dem enormen Speicherbedarf mehrdimensionaler Felder sorgt auch der relativ langsame Zugriff auf die einzelnen Elemente dafür, daß Felder mit mehr als zwei Dimensionen in der Praxis relativ selten vorkommen. Die Zugriffszeit ist eine Folge aufwendiger Index-Arithmetik. Faktisch werden alle Elemente eines mehrdimensionalen Arrays nicht als Tabellen, sondern der Reihe nach im Speicher abgelegt. Soll ein einzelnes Element geschrieben oder gelesen werden, muß dessen Adresse ausgehend von der Basisadresse des Feldes über die Multiplikation der Indexwerte errechnet werden.

Eine wesentlich elegantere Lösung für das Programmierproblem von array_3.c böte eine verkettete Liste von Strukturen an (siehe dazu Kapitel 13.3), deren Zahl sich abhängig von den eingegeben Daten dynamisch verändern ließe.

Initialisierung von Feldern

Es liegt nahe, die Elemente eines Arrays mit Hilfe einer Schleife initialisieren zu wollen. Beispielsweise könnte man die Elemente von iListe folgendermaßen mit dem Anfangswert 0 versehen:


.
.
#define GROESSE 10
.
int iListe[GROESSE], iCount;

for(iCount = 0; iCount < GROESSE; iCount++)
	iListe[iCount] = 0;
.
.

Für nicht allzu große Arrays bietet C allerdings eine bequemere Möglichkeit an, Arrays zu initialisieren. Gleich bei der Definition kann eine Liste von Anfangswerten zugewiesen werden; die einzelnen Werte müssen rechts vom Zuweisungsoperator in geschweiften Klammern stehen und durch Kommas voneinander getrennt sein. Beispiel:


int iListe[10] = {1,2,3,4,5,6,7,8,9,10};

Erwartungsgemäß enthält iListe[0] nach dieser Initialisierung den Wert 1, iListe[9] den Wert 10. Die Möglichkeiten der Array-Initialisierung sind damit aber noch nicht erschöpft: Besonders praktisch ist, daß Sie auf die Angabe der Arraygröße verzichten können, sobald Sie bei der Definition Anfangswerte angeben. Schließlich kann der Compiler anhand der Elementenliste schneller und ohne Fehler errechnen, wie groß das Array sein muß. Beispiel:


int iListe[] = {1,2,3,4,5,6,7,8};

Hier wird die Größe des Feldes automatisch auf 8 festgelegt. Sollten Sie allerdings vorhaben, später noch weitere Elemente hinzuzufügen, müssen Sie die Feldgröße gleich explizit höher setzen, da die maximale Anzahl der Elemente nachträglich generell nicht mehr geändert werden kann:


int iListe[15] = {1,2,3,4,5,6,7,8};

In diesem Beispiel kann das Array iListe 15 Elemente vom Typ int aufnehmen, wobei die ersten 8 gleich den entsprechenden Anfangswert erhalten; die verbleibenden Elemente werden mit dem Wert 0 initialisiert. Diese Eigenart bei der Initialiserung von Arrays kann dazu ausgenutzt werden, ohne großen Aufwand alle Elemente eines Feldes mit 0 zu initialisieren:


float fListe[15] = {0.0};
/* Die Initialisierung aller Elemente mit dem gleichen 
 * Wert funktioniert nur mit 0 */

Beachten Sie, daß das Feld fListe vom Typ float ist und daher mit der Fließkomma-Konstanten 0.0 initialisiert werden muß. Alle Elemente bekommen hier den Anfangswert 0.0.

Die Initialisierung von mehrdimensionalen Feldern funktioniert ähnlich, auf ihre Stelligkeit muß in der Notation der Anfangswerte natürlich Rücksicht genommen werden. Beispielsweise könnte das 2dimensionale Feld iTabelle wie folgt initialisiert werden:


int iTabelle[5][3] = {{1,2,3},
                      {2,3,4},
                      {3,4,5},
                      {4,5,6},
                      {7,8,9}};

Selbstverständlich könnten in einer formatfreien Sprache wie C alle Werte in eine Zeile geschrieben werden. Die oben verwendete Schreibweise erinnert aber an die Analogie mit den Zeilen und Spalten einer Tabelle und bewahrt vor einer verkehrten Initialisierung. Die Zuweisung der Anfangswerte muß nämlich in der Form Größe des 1. Indexes Zeilen und Größe des 2. Indexes Spalten erfolgen. Die Initialisierung


int iTabelle[5][3] = {{1,2,3,4,5}, /* falsch */
                      {2,3,4,5,6},
                      {3,4,5,6,7}};

wäre also falsch.

Auch bei der Initialisierung mehrdimensionaler Arrays besteht die Möglichkeit, die notwendige Anzahl an Elementen vom Compiler berechnen zu lassen. Allerdings darf nur die Größenangabe für den ersten Index ausgelassen werden:


/* leap.c */
#include <stdio.h>
int main(void){

int iTageProMonat[][13] = {
		 {0,31,28,31,30,31,30,31,31,30,31,30,31},
		 {0,31,29,31,30,31,30,31,31,30,31,30,31},
		 };

int iJahr, iSchaltJahr=0;

printf("Bitte eine Jahreszahl eingeben! ");
scanf("%4d", &iJahr);

if( (iJahr % 4 == 0 && iJahr % 100 != 0) ||
	iJahr % 400 == 0)

	iSchaltJahr = 1;

printf("Februar %d hatte %d Tage\n",
	iJahr, iTageProMonat[iSchaltJahr][2]);

return 0;
}

Dieses relativ triviale Beispiel zeigt, wie mit Hilfe eines zweidimensionalen Feldes die Anzahl der Tage pro Monat ermittelt werden kann. Bei der Initialisierung von iTageProMonat wäre es jedoch nicht zulässig, die Angabe 13 für den zweiten Index wegzulassen.

Unser letztes Beispiel zu Feldern errät die richtigen Lottozahlen für diese Woche. Es benutzt ein Array mit 50 integer-Elementen (bLookUp[49] für die höchstmögliche Zahl!) als Nachschlagetabelle, um zu verhindern, daß dieselbe Zahl mehrmals angekreuzt wird.


/* lotto.c
 * ermittelt die Lottozahlen 6 aus 49 und
 * sortiert diese in aufsteigender Reihenfolge
 * mit Bubble-Sort
 */

#include <stdio.h>
#include <stdlib.h>   /* wg. rand() und srand() */
#include <time.h>     /* wg. time() */

/* näheres zu #define in Kapitel 12 */
#define BOOL unsigned short

int main(void){

/* Nachschlagetabelle für vergebene Zahlen */
BOOL bLookUp[50] = { 0 };


/* Feld mit 6 Elementen für Lottozahlen */
int iLotto[6];

/* Hilfsvariablen für Schleifenzähler und bubble-sort */
int iCount, iCount1, iTemp;

for(iCount = 0; iCount < 6; iCount++){

	/* für jede Zahl Generator neu initialisieren */
	srand( (unsigned)time( NULL ) );

	/* die do-while Schleife sorgt dafür, daß für eine
	 * Lottozahl so lange ein neuer Wert erzeugt wird,
	 * bis dieser verschieden von allen bisher vergebenen
	 * Lottozahlen ist.
	 * Dies bewerkstelligt sie, indem bei jedem Durchlauf
	 * für die erzeugte Zahl in bLookUp nachgeschlagen
	 * wird.
	 * Z.B würde für den Wert 38 überprüft ob bLookUp[38]
	 * einen Wert ungleich 0 besitzt. Ist dies der
	 * Fall, dann muß ein neuer Wert gefunden werden.
	 */
		do{
			iLotto[iCount] = rand() % 49 + 1;
		}
		while( bLookUp[iLotto[iCount]] != 0 );

	/* Eintrag in der Nachschlagetabelle vornehmen.
	 * Wenn z.B.der Wert 38 vergeben wurde, dann erhält
	 * bLookUp[38] den Wert 1. Würde beim nächsten
	 * Schleifendurchlauf erneut die Zahl 38 gezogen,
	 * dann wäre die Bedingung der do-while-Schleife
	 * richtig und es müßte ein anderer Wert ermittelt
	 * werden.
	 */
	bLookUp[iLotto[iCount]] = 1;
	 }

/* Zahlen sortieren mit bubble-sort */

 for( iCount = 5; iCount >= 1; iCount--)
	for( iCount1 = 1; iCount1 <= iCount; iCount1++)
		 if(iLotto[iCount1 - 1] > iLotto[iCount1]){

			 iTemp = iLotto[iCount1 —
 1];
			 iLotto[iCount1 —
 1] = iLotto[iCount1];
			 iLotto[iCount1] = iTemp;
			}

 /* Lottozahlen ausgeben */
 printf("Die garantiert richtigen Lottozahlen:\n");
 for(iCount = 0; iCount <= 5; iCount++)
	printf("%d\t", iLotto[iCount]);

	putchar('\n');

return 0;
}

Auffällig ist die Flexiblität, mit der in C Indexwerte festgelegt werden können: So ist es sogar möglich, auf ein Element von bLookUp zuzugreifen, für das der Wert eines Elements aus einem anderen Array den Index liefert, also in der Form bLookUp[iLotto[iCount]].

Schließlich bleibt zu diesem Beispiel noch anzumerken, daß ein Sortieren von Daten (im Programm durch bubble-sort angezeigt) nur in Zusammenhang mit zusammengesetzten Datentypen sinnvoll ist. Der Versuch, eine Serie von einzelnen Variblen mit einem rationellen Verfahren zu sortieren, ist ziemlich aussichtlos.