12    Komplexe Datentypen

In diesem Kapitel erfahren Sie, wie Sie unterschiedliche primitive Datentypen zu einem einzigen Datentyp zusammenfassen können. Wenn Sie also auf der Suche nach einer komplexeren Datenstruktur sind, die unterschiedliche, aber logisch zusammenhängende Typen speichern kann, sind Sie bei den strukturierten Datentypen, auch oft nur Strukturen oder Structs genannt, genau richtig. Mit Strukturen können Sie aber nur komplexe Datenblöcke modellieren, indem Sie Variablen unterschiedlichen Typs zu einer Einheit zusammenfassen. Sie können aber nicht, da das Standard-C keine Objektorientierung unterstützt, z. B. Klassen definieren. Hierzu benötigen Sie die erweiterte C-Variante C++ oder eine andere objektorientierte Sprache wie z. B. Java. Selbstverständlich kann die Erläuterung des sehr komplexen Themas der objektorientierten Programmierung nicht in einem reinen C-Grundlagenbuch behandelt werden.

Neben Strukturen können Sie auch Unions bilden. Eine Union ist wie eine Struktur aufgebaut, nur haben die einzelnen Elemente (engl. members) einer Union immer die gleiche Startadresse. Folglich können Sie bei einer Union zwar ebenfalls mehrere Datentypen zusammenfassen, aber es kann immer nur ein Element genutzt werden.

Mit dem Aufzählungstyp enum können Sie Elementen mit einem Aufzählungstyp einen Namen zuordnen und sie mit diesem Namen verwenden. Intern sind die Aufzählungstypen als Ganzzahlen codiert.

Selbst definierte Datentypen

Sie können in C einen selbst definierten Datentyp aus einer Struktur (struct), einer Union (union) und dem Aufzählungstyp (enum) erstellen.

Einen Aliasnamen für existierende Datentypen oder Namen zu einem vereinbarten aggregierten Datentyp (z. B. einer Struktur) können Sie mit typedef vereinbaren. typedef kann eigentlich noch viel mehr, aber in diesem Grundkurs schildern wir nur die grundlegenden Dinge zu dieser Deklaration.

12.1    Strukturen

Mit den Strukturen können Sie einen eigenen Typ definieren, in dem Sie die einzelnen Daten und die Eigenschaften bestimmter Objekte beschreiben und zusammenfassen (z. B. Kundendaten einer Firma). Sie werden also zu Datensätzen (engl. records) zusammengefasst. Der Datensatz selbst besteht dann wiederum aus Datenfeldern mit den nötigen Informationen (Name, Adresse, Telefonnummer usw.). Die Datenfelder sind dann in C die Strukturvariablen und somit die Elemente einer Struktur.

12.1.1    Strukturtypen deklarieren

Die Deklaration eines Strukturtyps fängt immer mit dem Schlüsselwort struct an und enthält eine Liste von Elementen in geschweiften Klammern. Manche Compiler, wie z. B. der GCC in der Standardkonfiguration (also mit dem impliziten Flag std=C99), verlangen noch ein typedef vor dem Schlüsselwort struct, und der Name des Datentyps muss am Ende stehen. Ein strukturierter Datentyp wird stets mit einem Semikolon abgeschlossen. Innerhalb der geschweiften Klammern muss mindestens ein Element deklariert werden. Die Syntax einer solchen Struktur sieht folgendermaßen aus:

struct [bezeichner1] { Liste_von_Komponenten } [bezeichner2];

Die Komponenten der Struktur (auch Member oder Member-Variablen genannt) können einen beliebigen Datentyp enthalten, der natürlich auch zuvor definierte Strukturtypen beinhalten kann. Nicht erlaubt als Komponenten einer Struktur sind Funktionen, zumindest nicht in Standard-C.

Mit dem folgenden Strukturtyp Id3_tag werden Daten ähnlich den Metadaten einer MP3-Datei beschrieben:

struct Id3_tag {
char titel[30];
char kuenstler[30];
char album[30];
short jahr;
char kommentar[30];
char genre[30];
};

Wenn Ihr Compiler nun bei der Definition der gerade geschilderten Definition »meckert«, wie das z. B. bei uns der GCC auf dem Raspberry Pi tut, können Sie drei Dinge tun. Erstens können Sie beim Kompilieren -std=C11 mit angeben, zweitens können Sie (wenn alles nichts hilft) einen C++-Compiler verwenden (z. B. mit C++ structs.cpp -o structs) oder aber folgende Definition benutzen, die das Gleiche tut:

typedef struct {
char titel[30];
char kuenstler[30];
char album[30];
short jahr;
char kommentar[30];
char genre[30];
} Id3_tag;

Wie Sie im Endeffekt vorgehen, ist Geschmackssache, aber wenn Sie portable Programme erstellen müssen, verwenden Sie am besten die Variante mit dem typedef.

Mit der Typvereinbarung im letzten Beispiel haben Sie nun einen strukturierten Datentyp mit dem Bezeichner Id3_tag erstellt, der sich aus sechs Komponentenvariablen zusammensetzt und die typischen Informationen für ein MP3-Musikstück enthält. Wie Sie konkret mit dem Bezeichner Id3_tag arbeiten können, wird nun erklärt.

Was ist ein ID3-Tag?

ID3-Tags sind die Informationen (Metadaten), die in den MP3-Audiodateien enthalten sind (ID3 = Identify an MP3). Um allerdings vor falschen Erwartungen zu warnen: Die Struktur im Beispiel ist nicht völlig ID3-Tag-konform. Die einzelnen Datentypen (z. B. bei der Länge) wurden hier nicht ganz eingehalten. Falls Sie vorhaben, einen echten ID3-Tag-Editor zu erstellen bzw. die Tags auszulesen, finden Sie unter https://de.wikipedia.org/wiki/ID3-Tag weitere Informationen zu diesem Thema.

Wie bei den Basisdatentypen können Sie bei der Typvereinbarung Komponentenvariablen mit demselben Typ, getrennt durch ein Komma, zusammenfassen. Hier ein Beispiel:

struct Id3_tag {
char titel[30], kuenstler[30], album[30];
short jahr;
char kommentar[30], char genre[30];
};

Hier soll nochmals erwähnt werden, dass bisher nur die Rede von einer Deklaration ist, nicht aber von der Speicherplatzreservierung. Speicherplatz wird erst reserviert, wenn Sie eine Strukturvariable, also einen Bezeichner vom Strukturtyp definieren.

12.1.2    Definition einer Strukturvariablen

Die Definition einer Struktur erstellen Sie mithilfe eines Bezeichners. In diesem Fall spricht man von der Definition einer Strukturvariablen. In dem folgenden Beispiel werden beispielsweise drei Strukturvariablen mit dem Typ struct Id3_tag definiert. Dabei wird Speicherplatz für den Typ struct Id3_tag und seine einzelnen Komponentenvariablen reserviert:

// Definition einer Strukturvariablen
struct Id3_tag data1;
...
// Definition von zwei Strukturvariablen
struct Id3_tag data2, data3

12.1.3    Erlaubte Operationen auf Strukturvariablen

Folgende Operationen sind mit Strukturvariablen erlaubt:

Auf die Details der einzelnen Operationen wird auf den folgenden Seiten ausführlicher eingegangen.

12.1.4    Deklaration und Definition zusammenfassen

Die Deklaration eines Strukturtyps und die Definition einer Strukturvariablen können Sie auch zu einer Anweisung zusammenfassen:

(ggf. typedef) struct Id3_tag {
char titel[30];
char kuenstler[30];
char album[30];
short jahr;
char kommentar[30];
char genre[30];
} data;

Hiermit deklarieren Sie den Strukturtyp struct Id3_tag und definieren eine Strukturvariable namens data. Sie können auch mehrere Strukturvariablen auf einmal definieren:

(ggf. typedef) struct Id3_tag {
char titel[30];
char kuenstler[30];
char album[30];
short jahr;
char kommentar[30];
char genre[30];
} data1, data2, data3, data4, data5; // 5 Strukturvariablen

12.1.5    Synonyme für Strukturtypen erstellen

Wenn Sie nicht ständig das Schlüsselwort struct bei der Definition von Strukturvariablen verwenden möchten, können Sie auch hier mit typedef ein Synonym dafür erstellen. Der Vorteil ist hier, wie gesagt, dass die folgenden Definitionen auch auf einem C99-Compiler funktionieren. Es folgt ein Beispiel für eine solche Definition:

typedef struct Id3_tag Id3_t;

Durch typedef können Sie jetzt eine Struktur mit einer Strukturvariable folgendermaßen definieren:

Id3_t data1;   // Anstatt: struct Id3_tag data1;

Ein solches Synonym können Sie bei der Deklaration des Strukturtyps wie folgt erstellen:

typedef struct Id3_tag {
char titel[30];
char kuenstler[30];
char album[30];
short jahr;
char kommentar[30];
char genre[30];
} Id3_t;

Statt struct Id3_tag verwenden jetzt Sie nur noch Id3_t für den Strukturtyp.

typedef struct in C++

Wenn Sie einen C++-Compiler verwenden, können Sie sich das typedef vor struct oder das Schlüsselwort struct bei Deklarationen wie struct Id3_tag mydata sparen. C++ ergänzt jede Strukturdefinition automatisch durch ein typedef.

12.1.6    Selektion von Komponenten einer Strukturvariablen

Um auf die Komponentenvariablen einer Strukturvariablen zugreifen zu können, werden der Punktoperator und der Pfeiloperator verwendet. Auf den Pfeiloperator wird noch gesondert in Unterabschnitt »Selektion der Komponenten über den Pfeiloperator« in Abschnitt 12.1.12 eingegangen. Hier wird zunächst der Punktoperator behandelt. Dazu ein einfaches Beispiel:

Id3_t data;
...
strncpy(data.titel, "21st Century Breakdown", 29);
strncpy(data.kuenstler, "Green Day", 29);
...
data.jahr=2009;
...

Sie speichern die Strings "21st Century Breakdown" und "Green Day" in den Komponentenvariablen titel und kuenstler. Der Komponentenvariablen jahr übergeben Sie den Wert 2009.

Es folgt nun ein einfaches Beispiel, wie Sie den einzelnen Komponenten einer Strukturvariablen einen Inhalt hinzufügen und darauf zugreifen können:

00  // Kapitel12/mp3_verwaltung_1.c
01 #include <stdio.h>
02 #include <stdlib.h>
03 #define MAX 64

04 typedef struct Id3_tag {
05 char titel[MAX];
06 char kuenstler[MAX];
07 char album[MAX];
08 short jahr;
09 char kommentar[MAX];
10 char genre[MAX];
11 } Id3_t;

12 void output(Id3_t song) {
13 printf("\n\nDer Song, den Sie eingaben:\n");
14 printf("Titel : %s", song.titel);
15 printf("Künstler : %s", song.kuenstler);
16 printf("Album : %s", song.album);
17 printf("Jahr : %hd\n", song.jahr);
18 printf("Kommentar : %s", song.kommentar);
19 printf("Genre : %s", song.genre);
20 }

21 void dump_buffer(FILE *fp) {
22 int ch;
23 while( (ch = fgetc(fp)) != EOF && ch != '\n' )
24 /* Kein Anweisungsblock nötig */ ;
25 }

26 int main(void) {
27 Id3_t data;
28 printf("Titel : ");
29 if(fgets(data.titel,sizeof(data.titel),stdin)==NULL) {
30 printf("Fehler bei der Eingabe\n");
31 return EXIT_FAILURE;
32 }
33 printf("Kuenstler : ");
35 if(fgets(data.kuenstler,sizeof(data.kuenstler),stdin)
==NULL){
36 printf("Fehler bei der Eingabe\n");
37 return EXIT_FAILURE;
38 }
39 printf("Album : ");
40 if(fgets(data.album,sizeof(data.album),stdin)==NULL) {
41 printf("Fehler bei der Eingabe\n");
42 return EXIT_FAILURE;
43 }
44 printf("Jahr : ");
45 if (scanf("%hd", &data.jahr) != 1 ) {
46 printf("Fehler bei der Eingabe\n");
47 return EXIT_FAILURE;
48 }
49 dump_buffer(stdin);
50 printf("Kommentar : ");
51 if(fgets(data.kommentar,sizeof(data.kommentar),stdin)
== NULL ) {
52 printf("Fehler bei der Eingabe\n");
53 return EXIT_FAILURE;
54 }
55 printf("Genre : ");
56 if(fgets(data.genre,sizeof(data.genre),stdin)==NULL) {
57 printf("Fehler bei der Eingabe\n");
58 return EXIT_FAILURE;
59 }
60 // Daten der Struktur ausgeben
61 output(data);
62 return EXIT_SUCCESS;
63 }

Listing 12.1    mp3_verwaltung_1.c implementiert die erste Version einer einfachen Anwendung zur Verwaltung Ihrer MP3s und verwendet hierzu strukturierte Datentypen.

In Zeile (27) wird die Strukturvariable data vom Typ struct Id3_tag definiert, und in den Zeilen (28) bis (59) werden die einzelnen Elemente der Struktur mithilfe des Punktoperators eingelesen. Die Funktion dump_buffer() in Zeile (49) ist ein Workaround, um ein verbleibendes Newline-Zeichen aus dem Eingabepuffer nach scanf() aus der Standardeingabe (stdin) zu holen, damit dieses in der Standardeingabe befindliche Newline-Zeichen des Tastaturpuffers nicht für das nächste fgets() in Zeile (51) verwendet wird, denn ohne eine weitere Eingabe würde es dann zum übernächsten fgets() in Zeile (56) springen. Die Funktion, die dies leistet, wird in den Zeilen (21) bis (25) definiert.

Achtung vor fflush(stdin)

Anstelle des selbst geschriebenen Workarounds der Funktion dump_buffer() wird häufig eine Lösung mit fflush(stdin) verwendet. Auch wenn diese Funktion bei einigen Compilern prima funktioniert, ist diese Möglichkeit zum einen nicht portabel, und zum anderen ist das Verhalten nach dem C-Standard undefiniert.

In Zeile (61) wird die komplette Struktur als Kopie an die Funktion output() (Zeilen (12) bis (20)) übergeben. Dort zeigen wir, wie Sie mithilfe des Punktoperators die einzelnen Komponenten der Strukturvariablen ausgeben können.

Das Programm gibt bei der Ausführung z. B. Folgendes aus:

Titel     :  Heavy Cross
Künstler : Gossip
Album : Heavy Cross Single
Jahr : 2009
Kommentar : Cooler Punk-Rock
Genre : Alternative

Der Song, den Sie eingaben:
Titel : Heavy Cross
Künstler : Gossip
Album : Heavy Cross – Single
Jahr : 2009
Kommentar : Cooler Punk-Rock
Genre : Alternative

12.1.7    Strukturen initialisieren

Sie initialisieren eine Strukturvariable bei der Definition explizit, indem Sie eine Initialisierungsliste verwenden, in der die einzelnen Initialisierer, durch ein Komma getrennt, in geschweiften Klammern zugewiesen werden. Halten Sie hier unbedingt die richtige Reihenfolge der Deklaration der Struktur ein! Ebenso muss der Initialisierer demselben Typ wie bei der Deklaration entsprechen. Ein kurzer Codeausschnitt zeigt Ihnen, wie dies gemacht werden kann:

Id3_t data = {
"Kings and Queens",
"30 Seconds to Mars",
"This Is War",
2009,
"Komponist: Jared Leto",
"Alternative"
};

In der Liste enthält jede Komponentenvariable einen Initialisierer. Sie können aber auch weniger Initialisierer angeben, als es Komponentenvariablen in der Struktur gibt. Dennoch müssen Sie die richtige Reihenfolge und den Typ einhalten. Hier ein Beispiel:

Id3_t data = {
"Blue Oyster Cult",
"Dont fear the reaper"
};

Hier werden nur die ersten beiden Komponentenvariablen der Strukturvariablen initialisiert. Die restlichen Komponenten werden automatisch mit 0 bzw. NULL initialisiert. Sie können quasi mit einer leeren Initialisierungsliste alle Werte der Komponentenvariablen mit 0 bzw. NULL vorbelegen.

12.1.8    Nur bestimmte Komponenten einer Strukturvariablen initialisieren

Seit dem C99-Standard ist es möglich, nur bestimmte Komponenten einer Strukturvariablen zu initialisieren. Als Initialisierer muss ein sogenannter Elemente-Bezeichner verwendet werden, der wie folgt aussieht:

.strukturelement = wert

Ein Beispiel hierzu, bezogen auf die Struktur Id3_t (alias struct Id3_tag):

Id3_t data = {
.kuenstler = "Stanfour",
.album = "Wishing Yo Well",
.genre = "Rock"
};

Hier wurden die Elemente kuenstler, album und genre der Strukturvariablen initialisiert. Die restlichen Elemente werden automatisch mit 0 initialisiert. Der Clou dabei ist, dass Sie nicht einmal auf die richtige Reihenfolge der Elemente achten müssen. Folgende Initialisierung verursacht keinerlei Probleme:

Id3_t data = {
.genre = "Pop",
.jahr = 2010,
.kuenstler = "Goldfrapp",
.titel = "Rocket"
};

Sie können bei der Initialisierung aber auch den üblichen Weg gehen und nur für einzelne Komponenten den Elemente-Bezeichner verwenden. Dann müssen Sie allerdings teilweise die Reihenfolge beachten. Hier ein Beispiel:

Id3_t data = {
"Gypsy",
"Shakira",
.genre = "Pop"
};

Hier werden die ersten zwei Elemente wie mit einer üblichen Initialisierungsliste initialisiert und das letzte Element genre mithilfe des Elemente-Bezeichners. Die restlichen Werte werden automatisch mit 0 initialisiert.

12.1.9    Zuweisung bei Strukturvariablen

Auch eine Zuweisung von zwei Strukturvariablen von demselben Strukturtyp ist erlaubt. Eine solche Zuweisung kann beispielsweise wie folgt aussehen:

Id3_t data = { 
"Kings and Queens",
"30 Seconds to Mars",
"This Is War",
2009,
"Komponist: Jared Leto",
"Alternative"
};
Id3_t copyData;
...
copyData = data;

Bei einer solchen Zuweisung werden alle Komponentenwerte von data an die jeweiligen Komponentenvariablen von copyData zugewiesen.

12.1.10    Größe und Speicherausrichtung einer Struktur

Die Größe der Strukturvariablen eines Strukturtyps lässt sich nicht anhand der einzelnen darin enthaltenen Komponentenvariablen errechnen, weil bei dem eben erwähnten Padding-Verfahren (oder auch Alignment) die Komponenten auf eine bestimmte Größe »aufgefüllt« werden. Daher sollten Sie auch hier zur Ermittlung der Größe den sizeof-Operator verwenden (beispielsweise sizeof(struct Id3_tag)). Seit C11 können Sie auch mit dem _Alignof-Operator das Alignment der ganzen Struktur ermitteln.

12.1.11    Strukturen vergleichen

Da Strukturen für die Speicherausrichtung (engl. data alignment) das sogenannte Data-Structure-Padding-Verfahren verwenden (es gibt Lücken zwischen den Komponenten), gibt es keinen portablen Weg, mit dem Sie zwei Strukturvariablen desselben Typs direkt mit dem ==-Operator vergleichen können. Es bleibt Ihnen also nichts anderes übrig, als selbst eine Funktion zu schreiben, die zwei Strukturen Element für Element miteinander vergleicht. Auch bei Arrays war dies ja so.

12.1.12    Strukturen, Funktionen und Strukturzeiger

In unserem ersten Beispiel dieses Kapitels, mp3_verwaltung_1.c, wurde eine Struktur als Kopie an eine Funktion übergeben. Der Vorgang ist allerdings nicht sehr effektiv, weil Strukturen einen ziemlichen großen Datenumfang haben können. So hat die Struktur struct Id3_tag auf einem Testrechner hier beispielsweise 322 Bytes, die bei jedem Funktionsaufruf auf dem Stack kopiert und wieder freigegeben werden müssen. Bei häufigen Funktionsaufrufen wird deshalb die Laufzeit des Programms erheblich beeinträchtigt. Daher ist es oft wesentlich effizienter, hierzu Zeiger zu verwenden.

Zeiger auf Strukturvariablen

Zeiger auf Strukturvariablen eines bestimmten Strukturtyps lassen sich genauso realisieren wie Zeiger auf normalen Variablen von Basisdatentypen. Mit folgender Anweisung speichern Sie z. B. die Adresse der Strukturvariablen data im Strukturvariablenzeiger id3_ptr:

struct Id3_tag data = {
"Kings and Queens",
"30 Seconds to Mars",
"This Is War",
2009,
"Komponist: Jared Leto",
"Alternative"
};
struct Id3_tag *id3_ptr = NULL;
// id3_ptr verweist auf die Anfangsadresse von data
id3_ptr = &data;

Selektion der Komponenten über den Pfeiloperator

Wenn Sie einen Strukturvariablenzeiger verwenden, der die Adresse einer Strukturvariablen von demselben Strukturtyp enthält, erfolgt der Zugriff auf die einzelnen Komponentenvariablen etwas anders. Zwar können Sie auch den Dereferenzierungsoperator und den Punktoperator auf die gewünschte Komponente in der Struktur anwenden, aber Sie müssen dann eine Klammerung verwenden, weil der Punktoperator eine höhere Priorität als der Dereferenzierungsoperator (*) hat. Ein Zugriff auf eine einzelne Komponente über den Strukturvariablenzeiger müsste daher mit folgender Syntax realisiert werden:

(*struct_zeiger).komponente

Allerdings ist diese Art, einen Zeiger auf die einzelnen Komponenten zu verwenden, etwas umständlich. Glücklicherweise gibt es mit dem Pfeiloperator (->) eine einfache und äquivalente Alternative. Die Syntax mit dem Pfeiloperator sieht wie folgt aus:

struct_zeiger->komponente // == (*struct_zeiger).komponente

Mit diesen Kenntnissen soll jetzt ein Beispiel erstellt werden, mit dem eine Strukturvariable dynamisch zur Laufzeit in einer Funktion erzeugt und die Adresse an den Aufrufer zurückgegeben wird. Dieser Strukturvariablenzeiger wird außerdem noch an eine Funktion übergeben, und der Inhalt der einzelnen Komponenten wird ausgegeben.

00  // Kapitel12/mp3_verwaltung_2.c
01 #include <stdio.h>
02 #include <stdlib.h>
03 #define MAX 64

04 typedef struct Id3_tag {
char titel[MAX];
char kuenstler[MAX];
char album[MAX];
short jahr;
char kommentar[MAX];
char genre[MAX];
} Id3_t;

05 void dump_buffer(FILE *fp) {
06 int ch;
07 while( (ch = fgetc(fp)) != EOF && ch != '\n' )
08 /* Kein Anweisungsblock nötig */ ;
09 }

10 void print_id3(Id3_t* song) {
11 if(song == NULL ) {
12 printf("Kein Inhalt gefunden\n");
13 }
14 else {
15 printf("\n\nDer Song, den Sie eingaben:\n");
16 printf("Titel : %s", song->titel);
17 printf("Kuenstler : %s", song->kuenstler);
18 printf("Album : %s", song->album);
19 printf("Jahr : %hd\n", song->jahr);
20 printf("Kommentar : %s", song->kommentar);
21 printf("Genre : %s", song->genre);
22 }
23 }

24 Id3_t *new_id3(void) {
25 Id3_t* data = malloc(sizeof(Id3_t));
26 if( data == NULL ) {
27 printf("Fehler bei der Speicheranforderung\n");
28 return NULL;
29 }
30 printf("Titel : ");
31 if(fgets(data->titel,sizeof(data->titel),stdin)==NULL) {
32 printf("Fehler bei der Eingabe\n");
33 free(data); // Speicher vorher freigeben
34 return NULL; // NULL-Zeiger zurück
35 }
36 printf("Kuenstler : ");
37 if(fgets( data->kuenstler,
sizeof(data->kuenstler), stdin) == NULL ) {
38 printf("Fehler bei der Eingabe\n");
39 free(data);
40 return NULL;
41 }
42 printf("Album : ");
43 if(fgets(data->album,sizeof(data->album),stdin)==NULL) {
44 printf("Fehler bei der Eingabe\n");
45 free(data);
46 return NULL;
47 }
48 printf("Jahr : ");
49 if (scanf("%hd", &data->jahr) != 1 ) {
50 printf("Fehler bei der Eingabe\n");
51 free(data);
52 return NULL;
53 }
54 dump_buffer(stdin);
55 printf("Kommentar : ");
56 if(fgets( data->kommentar,
sizeof(data->kommentar), stdin) == NULL ) {
57 printf("Fehler bei der Eingabe\n");
58 free(data);
59 return NULL;
60 }
61 printf("Genre : ");
62 if(fgets(data->genre,sizeof(data->genre),stdin)==NULL){
63 printf("Fehler bei der Eingabe\n");
64 free(data);
65 return NULL;
66 }
67 return data;
68 }

69 int main(void) {
70 Id3_t *data = new_id3();
71 if( data != NULL ) {
72 print_id3(data);
73 }
74 free(data); // auch im Fall von NULL kein Problem
75 return EXIT_SUCCESS;
76 }

Listing 12.2    Die MP3-Verwaltung in mp3_verwaltung_2.c reserviert im Gegensatz zu mp3_verwaltung_1.c den Speicher für die Strukturen zur Laufzeit.

In Zeile (70) rufen Sie die Funktion new_id3() auf, deren Rückgabewert Sie dem Strukturvariablenzeiger data zuweisen. In der Funktion new_id3() selbst (Zeilen (24) bis (68)) wird zunächst in Zeile (25) ein Speicherblock vom Heap für eine neue Struktur angefordert. Anschließend werden in den Zeilen (30) bis (66) die einzelnen Elemente der dynamisch reservierten Strukturvariablen eingelesen. Das haben Sie bereits recht ähnlich im ersten Beispiel (mp3_verwaltung_1.c) dieses Kapitels gesehen. Hier wird jetzt allerdings der Pfeiloperator statt des Punktoperators für den selektiven Zugriff auf die einzelnen Komponenten der Strukturvariablen verwendet. In Zeile (67) wird die Anfangsadresse der kompletten Struktur an den Aufrufer (Zeile (70)) zurückgegeben.

In Zeile (72) wird die Adresse an die Funktion print_id3() (Zeilen (10) bis (23)) übergeben. In der Funktion print_id3() selbst wird dann nochmals demonstriert, wie mithilfe des Pfeiloperators auf die einzelnen Komponenten zugegriffen werden kann. Das Programm bei der Ausführung entspricht ansonsten dem Beispiel zuvor.

12.1.13    Array von Strukturvariablen

Im Grunde spricht nichts dagegen, ein Array von Strukturvariablen eines bestimmten Strukturtyps zu verwenden. Solche Arrays lassen sich nutzen wie Arrays mit gewöhnlichen Basisdatentypen. Für den Zugriff auf die einzelnen Komponentenvariablen wird hier der Punktoperator verwendet. Es folgt ein einfaches Beispiel dazu:

00  // Kapitel12/mp3_verwaltung_3.c
01 #include <stdio.h>
02 #include <stdlib.h>
03 #define MAX 64
04 #define SIZE 3

05 typedef struct Id3_tag {
char titel[MAX];
char kuenstler[MAX];
char album[MAX];
short jahr;
char kommentar[MAX];
char genre[MAX];
} Id3_t;

06 void dump_buffer(FILE *fp) {
07 int ch;
08 while( (ch = fgetc(fp)) != EOF && ch != '\n' )
09 /* Kein Anweisungsblock nötig */ ;
10 }

11 void print_id3(Id3_t* song) {
12 if(song == NULL ) {
13 printf("Kein Inhalt gefunden\n");
14 }
15 else {
16 printf("\n\nAusgabe der ID3-Daten:\n");
17 printf("Titel : %s", song->titel);
18 printf("Kuenstler : %s", song->kuenstler);
19 printf("Album : %s", song->album);
20 printf("Jahr : %hd\n", song->jahr);
21 printf("Kommentar : %s", song->kommentar);
22 printf("Genre : %s", song->genre);
23 }
24 }

25 int main(void) {
26 Id3_t data[SIZE];
27 for(int i=0; i<SIZE; i++) {
28 printf("Titel : ");
29 if(fgets( data[i].titel,
sizeof(data[i].titel),stdin) == NULL ) {
30 printf("Fehler bei der Eingabe\n");
31 return EXIT_FAILURE;
32 }
33 printf("Kuenstler : ");
34 if(fgets(data[i].kuenstler,
sizeof(data[i].kuenstler), stdin) == NULL ) {
35 printf("Fehler bei der Eingabe\n");
36 return EXIT_FAILURE;
37 }
38 printf("Album : ");
39 if( fgets(data[i].album,
sizeof(data[i].album), stdin) == NULL ) {
40 printf("Fehler bei der Eingabe\n");
41 return EXIT_FAILURE;
42 }
43 printf("Jahr : ");
44 if (scanf("%hd", &data[i].jahr) != 1 ) {
45 printf("Fehler bei der Eingabe\n");
46 return EXIT_FAILURE;
47 }
48 dump_buffer(stdin);
49 printf("Kommentar : ");
50 if( fgets(data[i].kommentar,
sizeof(data[i].kommentar), stdin) == NULL ) {
51 printf("Fehler bei der Eingabe\n");
52 return EXIT_FAILURE;
53 }
54 printf("Genre : ");
55 if( fgets(data[i].genre,
sizeof(data[i].genre), stdin) == NULL ) {
56 printf("Fehler bei der Eingabe\n");
57 return EXIT_FAILURE;
58 }
59 }
60 for(int i = 0; i<SIZE; i++ ) {
61 print_id3(&data[i]);
62 }
63 return EXIT_SUCCESS;
64 }

Listing 12.3    Das letzte Beispiel demonstriert die Verwendung von Struktur-Arrays.

In Zeile (26) wurde ein Array mit SIZE-Elementen vom Strukturtyp struct Id3_tag definiert. Die Werte der einzelnen Strukturelemente werden in den Zeilen (27) bis (59) übergeben. In den Zeilen (60) bis (62) werden die eingegebenen Werte von SIZE Strukturvariablen ausgegeben. Hierbei wird die im Listing zuvor erstellte Funktion print_id3() (Zeilen (11) bis (24)) verwendet, in der die Anfangsadresse des entsprechenden Elements im Array an die Funktion übergeben wird.

Die Verwendung von Arrays stellt keine allzu großen Anforderungen an den Programmierer. Sie sind einfach zu erstellen und zu handhaben. Allerdings sollten Sie sich immer vor Augen halten, dass Sie hiermit eine Menge Speicherplatz vergeuden können. Verwenden Sie ein Array für 1.000 Songtitel (was nicht unbedingt sehr viel ist), wird Speicher für 1.000 Titel verwendet, auch wenn vielleicht im Augenblick nur 100 benötigt werden. Und reichen dann 1.000 Songtitel nicht aus, müssen Sie sich wieder Gedanken über neuen Speicherplatz machen.

12.1.14    Strukturvariablen als Komponenten in Strukturen

Es spricht übrigens überhaupt nichts dagegen, Strukturvariablen als Komponenten in einem anderen Strukturtyp zu verwenden. Ein einfaches Beispiel ist das Ergänzen der MP3-Songtitel durch die Bitrate und die Spielzeit:

typedef struct Details {
short bitrate;
short spielzeit;
short abtastrate;
char version[MAX];
} Info_t;

typedef struct Id3_tag {
char titel[MAX];
char kuenstler[MAX];
char album[MAX];
short jahr;
char kommentar[MAX];
char genre[MAX];
Info_t info;
} Id3_t;

Der Strukturtyp struct Details (alias Info_t) wird als Komponentenvariable innerhalb der Struktur struct Id3_tag (alias Id3_t) verwendet. Um beispielsweise über eine Strukturvariable vom Typ Id3_t auf die Komponente bitrate vom Strukturtyp Info_t zugreifen zu können, müssen Sie im Fall einer Strukturvariable zweimal einen Punktoperator mehr verwenden. Dazu ein Beispiel:

Id3_t data;
...
data.info.bitrate = 256;

Ist data hingegen ein Zeiger, für den zuvor Speicher reserviert wurde, müssen Sie den Pfeiloperator an der richtigen Stelle verwenden:

data->info.bitrate = 256;

Es spricht auch nichts dagegen, die beiden Strukturtypen Id3_t und Info_t als Komponenten in einen neuen Strukturtyp zu packen:

typedef struct Meta_Info {
Id3_t id3;
Info_t info;
} Meta_t;

Abgesehen davon, dass Sie hierbei einen Operator zur Selektion der Komponenten mehr benötigen, ändert sich nicht sehr viel. Sehen Sie sich nachfolgend ein etwas umfangreicheres, aber trotzdem noch einfach gehaltenes Beispiel an, wie Sie auf die einzelnen Komponenten von mehrfach zusammengesetzten Strukturen zugreifen können:

00  // Kapitel12/mp3_verwaltung_4.c
01 #include <stdio.h>
02 #include <stdlib.h>
03 #define MAX 64

04 typedef struct Details {
unsigned short bitrate;
unsigned short spielzeit;
unsigned short abtastrate;
char version[MAX];
} Info_t;

05 typedef struct Id3_tag {
char titel[MAX];
char kuenstler[MAX];
char album[MAX];
short jahr;
char kommentar[MAX];
char genre[MAX];
Info_t info;
} Id3_t;

06 typedef struct Meta_Info {
Id3_t id3;
Info_t info;
} Meta_t;

07 void dump_buffer(FILE *fp) {
08 int ch;
09 while( (ch = fgetc(fp)) != EOF && ch != '\n' )
10 /* Kein Anweisungsblock nötig */ ;
11 }

12 void print_id3(Meta_t* song) {
13 if(song == NULL ) {
14 printf("Kein Inhalt gefunden\n");
15 }
16 else {
17 printf("\n\n");
18 printf("Titel : %s", song->id3.titel);
19 printf("Kuenstler : %s", song->id3.kuenstler);
20 printf("Album : %s", song->id3.album);
21 printf("Jahr : %hd\n", song->id3.jahr);
22 printf("Kommentar : %s", song->id3.kommentar);
23 printf("Genre : %s", song->id3.genre);
24 printf("Bitrate : %hu\n", song->info.bitrate);
25 printf("Spielzeit : %hu\n", song->info.spielzeit);
26 printf("Abtastrate: %hu\n", song->info.abtastrate);
27 printf("Version : %s", song->info.version);
28 }
29 }

30 int main(void) {
31 Meta_t data;
32 printf("Titel : ");
33 if( fgets(data.id3.titel,
sizeof(data.id3.titel), stdin) == NULL ) {
34 printf("Fehler bei der Eingabe\n");
35 return EXIT_FAILURE;
36 }
37 printf("Kuenstler : ");
38 if( fgets(data.id3.kuenstler,
sizeof(data.id3.kuenstler), stdin) == NULL ) {
39 printf("Fehler bei der Eingabe\n");
40 return EXIT_FAILURE;
41 }
42 printf("Album : ");
43 if( fgets(data.id3.album,
sizeof(data.id3.album), stdin) == NULL ) {
44 printf("Fehler bei der Eingabe\n");
45 return EXIT_FAILURE;
46 }
47 printf("Jahr : ");
48 if (scanf("%hd", &data.id3.jahr) != 1 ) {
49 printf("Fehler bei der Eingabe\n");
50 return EXIT_FAILURE;
51 }
52 dump_buffer(stdin);
53 printf("Kommentar : ");
54 if( fgets(data.id3.kommentar,
sizeof(data.id3.kommentar), stdin) == NULL ) {
55 printf("Fehler bei der Eingabe\n");
56 return EXIT_FAILURE;
57 }
58 printf("Genre : ");
59 if( fgets(data.id3.genre,
sizeof(data.id3.genre), stdin) == NULL ) {
60 printf("Fehler bei der Eingabe\n");
61 return EXIT_FAILURE;
62 }
63 printf("Bitrate : ");
64 if (scanf("%hu", &data.info.bitrate) != 1 ) {
65 printf("Fehler bei der Eingabe\n");
66 return EXIT_FAILURE;
67 }
68 dump_buffer(stdin);
69 printf("Dauer(sec): ");
70 if (scanf("%hu", &data.info.spielzeit) != 1 ) {
71 printf("Fehler bei der Eingabe\n");
72 return EXIT_FAILURE;
73 }
74 dump_buffer(stdin);
75 printf("Abtastrate: ");
76 if (scanf("%hu", &data.info.abtastrate) != 1 ) {
77 printf("Fehler bei der Eingabe\n");
78 return EXIT_FAILURE;
79 }
80 dump_buffer(stdin);
81 printf("Version : ");
82 if( fgets(data.info.version,
sizeof(data.info.version), stdin) == NULL ) {
83 printf("Fehler bei der Eingabe\n");
84 return EXIT_FAILURE;
85 }
86 print_id3(&data);
87 return EXIT_SUCCESS;
88 }

Listing 12.4    mp3_verwaltung_4.c ergänzt die MP3-Verwaltung durch die Angabe der Bitrate und Spielzeit unter Verwendung von Strukturen in Strukturen.

In Zeile (06) wurde der mehrfach zusammengesetzte Strukturtyp struct Meta_Info (alias Meta_t) mit den Strukturvariablen vom Typ Id3_t und Info_t als Komponenten deklariert. In Zeile (31) wird die Strukturvariable data mit dem Typ Meta_t definiert, und in den Zeilen (32) bis (85) werden die einzelnen Werte an diese verschachtelte Strukturvariable übergeben. Um auch den Zugriff über Zeiger auf die einzelnen Komponenten zu demonstrieren, wurde die Anfangsadresse dieser Struktur in Zeile (86) an die Funktion print_id3() (Zeilen (12) bis (29)) übergeben.

Das Programm gibt bei der Ausführung z. B. Folgendes aus:

Titel     :  Use Somebody
Künstler : Kings Of Leon
Album : Only By The Night
Jahr : 2008
Kommentar : Komponist: Nathan Followill
Genre : Alternative Rock
Bitrate : 256
Dauer(sec): 231
Abtastrate: 44100
Version : MPEG-1, Layer 3


Titel : Use Somebody
Künstler : Kings Of Leon
Album : Only By The Night
Jahr : 2008
Kommentar : Komponist: Nathan Followill
Genre : Alternative Rock
Bitrate : 256
Spielzeit : 231
Abtastrate: 44100
Version : MPEG-1, Layer 3

12.1.15    Zeiger als Komponenten in Strukturen

An dieser Stelle muss noch kurz auf Zeiger als Komponenten in einem Strukturtyp eingegangen werden – ein etwas komplexeres Thema. Hierbei gilt, dass Zeiger als Komponenten in einer Struktur ebenfalls lediglich wieder eine Anfangsadresse repräsentieren. Verwenden wir nun einen Zeiger als Komponente einer Struktur:

typedef struct Id3_tag {
char titel[MAX];
char kuenstler[MAX];
char album[MAX];
short jahr;
char kommentar[MAX];
char genre[MAX];
Info_t *info; // Zeiger als Komponente
} Id3_t;

In dem letzten Beispiel müssen Sie selbst dafür sorgen, dass Sie an die Struktur Info_t (alias struct Details) einen gültigen Inhalt zuweisen, indem Sie beispielsweise dynamisch Speicher zur Laufzeit reservieren oder eben eine mit Werten belegte Strukturvariable vom Typ Info_t mit dem Adressoperator zuweisen. Bezogen auf das Listing mp3_verwaltung_4.c könnten Sie folgendermaßen vorgehen:

// Kapitel5/mp3_verwaltung_5.c
...
Id3_t data;
...
data.info = malloc(sizeof(Info_t));
if(data.info != NULL ) {
printf("Bitrate : ");
if (scanf("%hu", &data.info->bitrate) != 1 ) {
printf("Fehler bei der Eingabe\n");
return EXIT_FAILURE;
}
dump_buffer(stdin);
...
}
else {
data.info = NULL; // falls kein Speicher reserviert wurde
}
...

Entsprechend erfolgt der Zugriff auf die einzelnen Komponenten mit dem Zeiger in der Struktur über den Pfeiloperator, wie hier mit &data.info ‐>bitrate demonstriert wurde. Der Zugriff von Strukturvariablenzeigern mit Komponenten als Zeigern in Strukturen erfolgt dann über zwei Pfeiloperatoren. Bezogen auf das Listing mp3_verwaltung_4.c in der Funktion print_id3() sieht dies so aus:

// Kapitel12/mp3_verwaltung_5.c
...
void print_id3(Id3_t* song) {
if(song == NULL ) {
printf("Kein Inhalt gefunden\n");
}
else {
printf("\n\n");
printf("Titel : %s", song->titel);
printf("Kuenstler : %s", song->kuenstler);
printf("Album : %s", song->album);
printf("Jahr : %hd\n", song->jahr);
printf("Kommentar : %s", song->kommentar);
printf("Genre : %s", song->genre);
if(song->info != NULL ) {
printf("Bitrate : %hu\n", song->info->bitrate);
printf("Spielzeit : %hu\n", song->info->spielzeit);
printf("Abtastrate: %hu\n", song->info->abtastrate);
printf("Version : %s", song->info->version);
}
}
}

Der Vorteil solcher Zeiger in Strukturtypen, die gegebenenfalls selbst Strukturen sind, ist, dass sich damit Speicherplatz sparen lässt. Wird beispielsweise diese mehrfach zusammengesetzte Struktur nicht benötigt, wird auch weniger Speicherplatz benötigt. Nochmals bezogen auf das Listing mp3_verwaltung_4.c könnten Sie somit folgende mehrfach zusammengesetzte Struktur mit Zeigern als Komponenten deklarieren:

Typedef struct Meta_Info {
Id3_t *id3;
Info_t *info;
} Meta_t;

...
Meta_t data = { NULL, NULL };

Natürlich erhöht sich mit Zeigern als Komponenten von zusammengesetzten Strukturen auch die Komplexität des Programms, weil Sie sich zusätzlich noch u. a. um die Reservierung und Freigabe von Speicher kümmern müssen. Auch müssen Sie immer sicherstellen, dass Zeiger auf gültige Speicherbereiche verwendet werden.

Achtung: Vorsicht bei Zuweisungen

Wenn ein Strukturtyp einen Zeiger als Komponente enthält, müssen Sie bei einer Zuweisung vorsichtig sein, weil hierbei nicht der Inhalt, sondern nur die Adresse des Zeigers kopiert wird, auf die dieser zeigt. Das bedeutet, dass zwei Strukturvariablen mit einem Zeiger als Komponente nach einer Zuweisung auf dieselbe Adresse verweisen.