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:
D | i | e | s | i | s | t | e | i | n | T | e | x | t | ! | \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