7. Strukturierte Datentypen

7.1. Arrays

Ein Array ist eine Sammlung bzw. eine Tabelle mit Variablen des gleichen Datentyps, die über einen gemeinsamen Variablennamen angesprochen werden. Jede Variable des Arrays ist von den anderen unabhängig und wird als Element bezeichnet. Der Datentyp ist dabei beliebig, er kann auch selber ein strukturierter Datentyp sein, z.B. ein Array von Arrays. Innerhalb des Arrays wird mit Hilfe einer Positionsangabe, dem Index, auf ein Element zugegriffen. Der Index zählt immer ab Null!

Ein Array wird folgendermaßen definiert:

Datentyp Arrayname[Anzahl_der_Elemente]

Beispiel:

int Zahlen[10];

Im Beispiel wird ein Array von ganzen Zahlen (Integer) namens Zahlen mit 10 Elementen angelegt. Die einzelnen Elemente werden mit Zahlen[0] ... Zahlen[9] angesprochen. Das Element Zahlen[10] gibt es nicht, da es bereits das 11. Element wäre. Allerdings wird weder vom Compiler noch zur Laufzeit überprüft, ob die Array-Grenzen überschritten werden. Daher muss der Programmierer immer selber sicherstellen, dass dies nicht geschieht.

Arrays werden meist mit Schleifen behandelt, weil häufig für alle Elemente des Arrays die gleichen Operationen durchgeführt werden müssen. Im folgenden Beispiel werden 10 Zahlen in ein Array eingelesen und anschließend die Summe der 10 Zahlen ausgegeben.

Beispiel:

kap07_01.c

01 /***********************************************
02  * Dieses Programm liest zehn ganze Zahlen ein
03  * und ermittelt die Summe der zehn Zahlen.
04  * Eingabe: Zahlen[0], ..., Zahlen[9]
05  * Ausgabe: Summe
06  ***********************************************/
07 #include <stdio.h>
08
09 int main()
10 {
11    int Zahlen[10], i, Summe;
12
13    printf("Dieses Programm ermittelt die Summe ");
14    printf("von zehn eingegebenen Zahlen.\n");
15    printf("Bitte geben Sie zehn ganze Zahlen ein: ");
16    for (i = 0; i < 10; i++)
17       scanf("%d", &Zahlen[i]);
18    for (i = 0, Summe = 0; i < 10; i++)
19    {
20       printf("%i", Zahlen[i]);
21       printf(i < 9 ? " + " : " = ");
22       Summe += Zahlen[i];
23    }
24    printf("%i\n", Summe);
25
26    return 0;
27 }

Die Gesamtgröße des Arrays (d.h. der für das Array benötigte Speicherplatz) ergibt sich aus der Anzahl der Elemente multipliziert mit der Größe eines Elementes. Um die Größe eines Elementes zu erhalten, kann der Befehl sizeof(Datentyp) bzw. sizeof(Variable) verwendet werden.

Beispiel:

printf("Größe des Arrays: %i", 10 * sizeof(int));      /* bzw. */
printf("Größe des Arrays: %i", 10 * sizeof(Zahlen[0]));


Daraus ergibt sich eine Arraygröße von 40 Bytes (ein int hat 4 Bytes).

Arrays lassen sich genauso wie alle anderen Variablen bei der Definition auch gleich initialisieren. Dazu werden alle Werte für das Array jeweils mit einem Komma getrennt in geschweifte Klammern gestellt. Das sieht dann so aus:

Beispiel:

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

Die Anzahl der Elemente des Arrays und die Anzahl der Initialisierungswerte muss übereinstimmen. Da der Computer mitzählt, wieviele Elemente initialisiert werden, kann bei der Initialisierung die Anzahl der Elemente in den eckigen Klammern entfallen, also:

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

Es lassen sich auch mehrdimensionale Arrays definieren. Dabei muss für jede Dimension die Anzahl der Elemente in eckigen Klammern angegeben werden, und zwar so, dass jede Dimension ein eigenes Klammerpaar hat, z.B. Matrix[3][3]. Dagegen wäre Matrix[3,3] falsch (ist identisch mit Matrix[3]).

Beispiel:

kap07_02.c

01 /***********************************************
02  * Dieses Programm gibt die Zahlen einer
03  * 3x3-Matrix auf dem Bildschirm aus.
04  * Eingabe: keine
05  * Ausgabe: alle Werte der 3x3-Matrix
06  ***********************************************/
07 #include <stdio.h>
08
09 int main()
10 {
11    int i, j, Matrix[3][3] = { {1, 2, 3}, {4, 5, 6}, {7, 8, 9} };
12
13    /* Ausgabe der 3*3-Matrix: */
14    for (i = 0; i < 3; i++)
15    {
16       for (j = 0; j < 3; j++)
17          printf("%3i ", Matrix[i][j]);
18       printf("\n"); /* Zeilenumbruch nach jeder Matrixzeile */
19    }
20
21    return 0;
22 }

Mit dem C99-Standard ist die Möglichkeit hinzugekommen, Arrays mit einer variablen Anzahl von Elementen anzulegen. D.h. zur Zeit des Compilierens ist die Anzahl der Elemente des Arrays noch nicht bekannt. Ein solches Array wird wie die bisherigen Arrays definiert, nur die Anzahl der Elemente ist ein nicht-konstanter Ausdruck. Zur Laufzeit des Programms wird bei der Definition des Arrays der nicht-konstante Ausdruck (muss eine positive ganze Zahl sein!) ausgewertet und dann das Array mit der ermittelten Elementanzahl angelegt. Ist das Array erst einmal definiert, kann die Anzahl der Elemente nicht mehr geändert werden; das Array ist also kein dynamisches Array!

Beispiel:

kap07_03.c

01 #include <stdio.h>
02
03 int main()
04 {
05    int Anzahl = 0, i;
06
07    printf("Wie viele Elemente? ");
08    scanf("%i", &Anzahl);
09
10    if (Anzahl > 0)
11    {
12       int Array[Anzahl]; /* funktioniert erst ab C99! */
13
14       for (i = 0; i < Anzahl; i++)
15          Array[i] = i + 1;
16       for (i = 0; i < Anzahl; i++)
17          printf("Array[%i] = %i\n", i, Array[i]);
18    }
19
20    return 0;
21 }

Arrays mit variabler Anzahl von Elementen dürfen nicht als static- oder extern-Variablen angelegt werden; auch dürfen sie nicht als globale Variablen angelegt werden.

Wird ein neuer Datenyp mittels typedef anhand eines Arrays mit variabler Elementanzahl definiert, wird die Anzahl der Elemente für das Array beim Abarbeiten des typedef ermittelt. Der so definierte Datentyp ändert sich dann nicht mehr!

Beispiel:

kap07_04.c

01 #include <stdio.h>
02
03 int main()
04 {
05    int Anzahl = 5;
06
07    typedef int Array[Anzahl];
08
09    Anzahl++;
10
11    Array A;        /* Array mit 5 Elementen */
12    int B[Anzahl];  /* Array mit 6 Elementen */
13
14    return 0;
15 }

Im Kapitel Zeiger wird noch einmal auf Arrays eingegangen.

7.2. Zeichenketten (Strings)

Eine Zeichenkette (auch String genannt) ist ein Spezialfall eines Arrays, nämlich ein eindimensionales Array von Zeichen (char). In jedem Element des Arrays wird genau ein Zeichen abgelegt. Alle Elemente zusammen ergeben die Zeichenkette, z.B. ein Satz. Damit eignen sich Zeichenketten zum Speichern von Texten, Meldungen, usw. Dabei kann mit dem Array auf jeden Buchstaben einzeln zugegriffen werden.

Damit im Programm das Textende eindeutig erkannt werden kann, wird nach dem letzten Zeichen der Zeichenkette noch ein weiteres Element mit dem Zeichen '\0' (ASCII-Wert: 0) gespeichert. Das Array muss also immer ein Element mehr haben als die Zeichenkette lang ist!!! Wird dieses nicht beachtet, wird das Programm fehlerhaft arbeiten oder sogar abstürzen!

Natürlich können Zeichenketten wie gewöhnliche Arrays bei der Definition initialisiert werden. Das würde dann so aussehen:

char Text[] = {'D','i','e','s',' ','i','s','t',' ',
               'e','i','n',' ','T','e','x','t','!','\0'};


Damit wird ein Array mit 19 Elementen (18 Textzeichen und 1 Abschlusszeichen) erzeugt. Tabellarisch sieht das folgendermaßen aus:

Dies  ist  ein  Text!\0

Damit die Initialisierungen von Zeichenketten nicht immer buchstabenweise erfolgen müssen, werden die Texte einfach in Anführungsstriche gesetzt. Das obige Array lässt sich dadurch einfacher initialisieren:

char Text[] = "Dies ist ein Text!";

Durch die Anführungsstriche setzt der Compiler automatisch das Abschlusszeichen an den Text heran; der Programmierer braucht sich an dieser Stelle darum nicht mehr zu kümmern.

Gefährlich wird es, wenn eine Zeichenkette zum Einlesen von Texten benutzt wird. Genau wie bei den Arrays wird weder vom Compiler noch zur Laufzeit getestet, ob die Zeichenkette für den eingegebenen Text groß genug ist! Wird ein Text über die Größe des Arrays hinweg eingelesen, werden im Normalfall die im Speicher hinter dem Array abgelegten Variablen überschrieben (das Programm liefert dann u.U. falsche Ergebnisse!); im ungünstigsten Fall stürzt das Programm ab (Speicherzugriffsfehler)! Zum Einlesen von Zeichenketten sollten also die Arrays ausreichend dimensioniert werden. Ferner sollte beim scanf die maximale Anzahl von erlaubten Zeichen angegeben werden. Alle weiteren eingegebenen Zeichen bleiben im Tastaturpuffer.

Beispiel:

kap07_05.c

01 /*********************************************
02  * Dieses Programm liest Ihren Namen ein und
03  * gibt ihn wieder auf dem Bildschirm aus.
04  *********************************************/
05 #include <stdio.h>
06
07 int main()
08 {
09    char Name[100];      /* Name darf max. 99 Zeichen lang */
10                         /* sein + 1 Abschlusszeichen */
11
12    printf("Geben Sie bitte Ihren Namen ein: ");
13    scanf("%99s", Name); /* KEIN & vor dem Variablennamen!!! */
14    printf("So, so, Sie heissen also %s\n", Name);
15
16    return 0;
17 }

Dadurch, dass die Eingabe nach jedem "weissen Leerzeichen" endet, kann auf diese Art und Weise nicht Vor- und Nachname zusammen eingegeben werden. In dem Array wird nur der Vorname bis zum ersten Leerzeichen übernommen. Darauf wird hier aber nicht weiter eingegangen.

Eine Zeichenkette lässt sich nicht direkt einer anderen Zeichenkette zuweisen, sondern muss zeichenweise kopiert werden. Dies wird im folgenden Beispiel gezeigt.

Beispiel:

kap07_06.c

01 /********************************************
02  * Dieses Programm kopiert einen Text einer
03  * Zeichenkette in eine andere.
04  ********************************************/
05 #include <stdio.h>
06
07 int main()
08 {
09    char Kette1[] = "Ein Text!", Kette2[100];
10    int i = 0;
11
12    printf("Der Text von Kette1 (%s) ", Kette1);
13    printf("wird nach Kette2 kopiert.\n");
14    while (Kette1[i] != '\0')
15    {
16       Kette2[i] = Kette1[i];
17       i++;
18    }
19    Kette2[i] = '\0';  /* Abschlusszeichen setzen */
20    printf("Inhalt von Kette2: %s\n", Kette2);
21
22    return 0;
23 }

Nun kommen wieder einige Abkürzungen und Kurzformen, die hier Schritt für Schritt erläutert werden. Im ersten Schritt wird die Bedingung der while-Schleife verkürzt. Die Bedingung ist ein logischer Ausdruck, der entweder gleich Null (entspricht falsch) oder ungleich Null bzw. 1 (entspricht wahr) ist. Die Bedingung in while (Kette[i] != '\0') fragt nach dem Nullzeichen '\0'. Da das Nullzeichen auch den ASCII-Wert 0 hat, kann die Bedingung auch als logische Abfrage gelesen werden: Solange Kette1[i] ungleich falsch, dann ... D.h. steht in Kette1[i] das Nullzeichen, ist bereits der Ausdruck Kette1[i] falsch! Wird noch das i++; in die Zuweisung der vorigen Zeile mit hineingeschrieben, sieht die while-Schleife folgendermaßen aus (der Rest ändert sich nicht.):

while (Kette1[i])
   Kette2[i] = Kette1[i++];
Kette2[i] = '\0';  /* Abschlusszeichen setzen */


Insgesamt sind damit bereits zwei Zeilen entfallen. Im zweiten und schon letzten Schritt wird ausgenutzt, dass das Ergebnis der Zuweisung Kette2[i] = Kette1[i++]; gleich dem Wert von Kette1[i] ist (durch das i++ wird die Variable i erst nach der Zuweisung um eins erhöht). Ist also das Ergebnis der Zuweisung gleich Null, ist das Ende der Zeichenkette erreicht. Da die Zuweisung aber vor der Abfrage durchgeführt wird, wird auch das Nullzeichen automatisch in die andere Zeichenkette mit kopiert. Damit entfällt das Setzen des Abschlusszeichens. Damit ist die while-Schleife zum Kopieren von Texten ein Zweizeiler geworden (nicht vergessen: die while-Schleife benötigt damit eine Leeranweisung (Semikolon)) :

while (Kette2[i] = Kette1[i++])
   ;


So wie das hier gezeigte Kopieren von Texten werden alle Operationen mit Texten programmiert: Eine while-Schleife mit der Abbruchbedingung "solange, bis Nullzeichen (Textende)" und innerhalb der Schleife jedes einzelne Zeichen bearbeiten. In der Bibliothek string.h stehen dem Programmierer bereits eine ganze Reihe von Textoperationen zur Verfügung. Diese Bibliothek wird genauso wie die Standardbibliothek stdio.h eingebunden, nämlich durch die Zeile
#include <string.h>

7.3. structs

Bei Arrays werden viele Elemente desselben Datentyps zusammengefasst. Häufig ist aber gewünscht, auch Daten unterschiedlicher Typen zu einem Objekt zusammenzufassen, wenn diese logisch zusammengehören. Die Lösung dazu hat in C/C++ den Namen structure, kurz struct und ist folgendermaßen definiert:

struct [Typname]
{
   Aufbau
} [Variablenliste];


Man kann sich dies gut als Karteikarte vorstellen. Um zum Beispiel eine Bücherverwaltung für die Bibliothek zu erstellen, müssen die Daten eines Buches - Titel, Autor, Standort, usw. - als ein Objekt behandelt werden. Diese einzelnen Daten werden im Aufbau wie gewöhnliche Variablen angegeben. Im folgenden Beispiel wird mit Hilfe von struct ein Datentyp namens Buch (Typname) definiert; gleichzeitig wird ein Array dieses Datentyps mit dem Namen Buecher erstellt (Variablenliste).

Beispiel:

struct Buch
{
   char Titel[100];
   char Autor[100];
   char ISBN[20];
   char Standort[10];
   float Preis;
} Buecher[50];     /* 50mal struct Buch */


Wahlweise kann der Typname oder die Variablenliste weggelassen werden. Wird der Typname weggelassen, werden Variablen des strukturierten Datentyps definiert, aber dieser strukturierte Datentyp hat keinen Namen. Dadurch können später im Programm keine weiteren Variablen dieses Datentyps definiert werden, es sei denn, es wird wieder die komplette Typdefinition geschrieben.
Wird anders herum die Variablenliste weggelassen, wird ein strukturierter Datentyp mit Namen definiert; es werden aber keine Variablen dieses Typs definiert. Dies kann aber später im Programm noch erfolgen, wie das nächste Beispiel unten zeigt.

Auf die Elemente (auch Felder oder Komponenten genannt) eines strukturierten Datentyps kann nicht direkt zugegriffen werden, weil sie nur in Verbindung mit dem Objekt existieren. Die Anweisung Preis = 19.99; ist unsinnig, weil nicht klar ist, welches Buch diesen Preis hat. Der Zugriff auf die Elemente einer Variablen vom Typ struct geschieht über einen Punkt zwischen Variablen- und Elementnamen, z.B. Buch1.Preis = 19.99;. Dann gilt der Preis für das Buch Buch1.

Beispiel:

kap07_07.c

01 /****************************************
02  * Dieses Programm legt zwei Variablen
03  * eines strukturierten Datentyps an.
04  ****************************************/
05 #include <stdio.h>
06 #include <string.h>
07
08 int main()
09 {
10    struct Buch
11    {
12       char Titel[100];
13       char Autor[100];
14       char Standort[10];
15       float Preis;
16    } Buch1;           /* Variablen-Definition direkt    */
17                       /* bei der Definition des structs */
18    struct Buch Buch2; /* Definition ueber Typnamen      */
19
20    strcpy(Buch1.Titel, "Das indische Tuch");
21    strcpy(Buch1.Autor, "Edgar Wallace");
22    strcpy(Buch1.Standort, "Regal 5");
23    Buch1.Preis = 14.99;
24
25    printf("Das Buch '%s' von '%s' ", Buch1.Titel, Buch1.Autor);
26    printf("kostet %.2f EUR.\n", Buch1.Preis);
27
28    return 0;
29 }


7.4. Aufzählungstypen

Häufig gibt es nicht-numerische Wertebereiche. So kann beispielsweise ein Wochentag nur die Werte Sonntag, Montag, ..., Samstag annehmen. Eine mögliche Hilfskonstruktion ist das Verwenden von ganzen Zahlen (int), wobei jeweils eine Zahl einem nicht-numerischen Wert entspricht (z.B. 0 = Sonntag, 1 = Montag, usw.). Dabei muss die Bedeutung der einzelnen Zahlen irgendwo als Kommentar festgehalten werden. Dadurch ist das Programm wieder deutlich schlechter lesbar. Auch ist die Zuordnung an einen nicht-numerischen Wert nicht eindeutig. Sind z.B. noch die Monate auf die gleiche Art und Weise in Zahlen "verschlüsselt", ist nicht klar, ob die Zahl 1 nun ein Montag oder der Januar ist. Und schließlich ist es möglich, einer Wochentag-Variablen auch den Wert 27 zuzuweisen, der keinem Wochentag entspricht.

Die Lösung für solche Fälle sind Aufzählungstypen (manchmal auch Enumerationstypen genannt). Die Syntax dazu sieht so aus:

enum [Typname] {Aufzählung} [Variablenliste];

Ähnlich wie bei struct kann hier wahlweise der Typname oder die Variablenliste weggelassen werden. Sinnvoll ist meist nur das Weglassen der Variablenliste. Das Weglassen des Typnamens ist nur dann sinnvoll, wenn der Aufzählungstyp nur für eine Variablendefinition benötigt wird. Dies wird auch anonyme Typdefinition genannt.

Beispiel:

enum Wochentag {Sonntag, Montag, Dienstag, Mittwoch,
                Donnerstag, Freitag, Samstag};


Damit ist der Aufzählungstyp definiert. Nun lassen sich Variablen dieses Typs definieren:

enum Wochentag Werktag, Feiertag, Heute = Dienstag;

Diesen Variablen können nur Werte aus der Werteliste des Aufzählungstypen zugewiesen werden. Intern werden sie auf die ganzen Zahlen - beginnend bei 0 - abgebildet. Dadurch ist eine implizite Typumwandlung von einem Aufzählungstypen zu den ganzen Zahlen möglich, aber nicht umgekehrt!

Werktag = Montag;          /* richtig */
Feiertag = Sonntag;        /* richtig */
int i = Freitag;           /* richtig (implizite Typumwandlung) */
Heute = 4;                 /* falsch! (keine Typumw. möglich!) */
int i = Montag + Dienstag; /* richtig (aber sinnlos) */
Heute = Montag + Dienstag; /* falsch! */
Heute++;                   /* falsch! */


Die Werte der Werteliste von Aufzählungstypen sind Konstanten und können nach der Definition nicht mehr verändert werden. Wohl aber kann bei der Definition des Aufzählungstypen den Werten andere Zahlen zugewiesen werden. Dazu wird jedem Wert gleich die zugehörige Zahl zugewiesen. Jede Zahl darf dabei natürlich nur einmal verwendet werden.

Beispiel:

enum Farben {weiss = 0, blau = 2, gruen = 5, rot = 25,
             gelb = 65, lila = 90, pink = 99};


Da die Zahlenwerte der Aufzählungstypen Konstanten sind (d.h. sie haben nur einen Namen und einen Wert, aber keine Speicheradresse), können Aufzählungstypen auch für die Definition von Zahlen-Konstanten verwendet werden.

Beispiel:

#define MAX 20
// ist demnach gleichwertig mit
enum { MAX = 20 };


7.5. Unions

Unions sind Strukturen, in denen die verschiedenen Elemente denselben Speicherplatz bezeichnen, d.h. die Elemente werden alle überlagert. Der benötigte Speicherplatz einer Variablen des Typs union ist identisch mit dem Speicherplatzbedarf des größten Elements.

Die Syntax, der Aufbau und der Zugriff auf Elemente des Typs union ähneln sehr stark denen von struct:

union [Typname]
{
   Aufbau
} [Variablenliste];


Im folgenden Beispiel werden eine int-Zahl und ein char-Array überlagert. Da jedes char nur ein Byte belegt, kann das Array genauso viele Elemente haben wie die int-Zahl Bytes belegt, nämlich sizeof(int).

Beispiel:

kap07_08.c

01 #include <stdio.h>
02
03 int main()
04 {
05    int i;
06    union Ueberlagerung
07    {
08       int Zahl;
09       unsigned char c[sizeof(int)];
10    } u;
11
12    do
13    {
14       printf("Zahl eingeben (0 = Ende): ");
15       scanf("%i", &u.Zahl);
16       printf("\nByteweise Darstellung von %i:\n", u.Zahl);
17       for (i = sizeof(int) - 1; i >= 0; i--)
18          printf("Byte Nr: %i = %i\n", i, u.c[i]);
19       printf("\n");
20    } while (u.Zahl);
21
22    return 0;
23 }

Die beiden Ausdrücke u.Zahl und u.c[] beziehen sich auf denselben Bereich im Speicher, nur die Interpretation ist unterschiedlich. Dieses Beispiel gibt die ganze Zahl Byte für Byte aus, so wie sie im Speicher steht.

7.6. Bitfelder

Gerade in der hardwarenahen und Systemprogrammierung werden häufig Bitfelder benötigt, in denen die einzelnen Bitpositionen unterschiedliche Bedeutungen haben. Ein Bitfeld ist in C/C++ ein struct, bei dem bei jedem Element - getrennt mit einem Doppelpunkt - die benötigte Anzahl von Bits dahintergeschrieben wird: Datentyp Elementname: Konstante;, wobei der Datentyp nur einer der Datentypen char, int, long sowie deren unsigned-Varianten und ein Aufzählungstyp sein darf.

Beispiel:

struct Bitfeld
{
   unsigned int a: 2;
   unsigned int b: 2;
   unsigned int c: 4;
} abc;


Auf die Elemente dieser struct-Variable kann wie mit jedem anderen struct umgegangen werden. Zu beachten sind allerdings die Zahlenbereiche der einzelnen Elemente: a und b haben jeweils den Zahlenbereich von 0 bis 3 (2-Bit-Zahlen) und das Element c den Zahlenbereich von 0 bis 15 (4-Bit-Zahl).

Zusammen sind es 8 Bit. Es dürfen allerdings keine Annahmen gemacht werden, wo diese 8 Bit innerhalb eines 16- bzw. 32-Bit-Wortes angeordnet sind. Dies kann von Compiler zu Compiler verschieden sein. Bitfelder sollten nicht zum Sparen von Arbeitsspeicher verwendet werden, weil der Aufwand des Zugriffs auf einzelne Bits beträchtlich sein kann. Und es nicht einmal sicher, ob wirklich Speicher gespart wird: Es könnte sein, dass ein Compiler alle Bitfelder - auch die der Länge 1 - stets an einer 32-Bit-Wortgrenze beginnen lässt. Üblich ist jedoch (ohne Garantie), dass aufeinanderfolgende Bitfelder aneinandergereiht werden.



Voriges Kapitel: 6. Kontrollstrukturen