C-Programmierung: Komplexe Datentypen

Strukturen

Strukturen fassen mehrere primitive oder komplexe Variablen zu einer logischen Einheit zusammen. Die Variablen dürfen dabei unterschiedliche Datentypen besitzen. Die Variablen der Struktur werden als Komponenten (engl. members) bezeichnet.

Eine logische Einheit kann zum Beispiel eine Adresse, Koordinate, Datums- oder Zeitangabe sein. Ein Datum besteht beispielsweise aus den Komponenten Tag, Monat und Jahr. Die Deklaration einer solchen Struktur sieht wie folgt aus:

struct datum
{
  int tag;
  char monat[10];
  int jahr;
};

Vergessen Sie bei der Deklaration bitte nicht das Semikolon am Ende!

Es gibt mehrere Möglichkeiten, Variablen von diesem Typ zu erzeugen; beispielsweise im Zuge der Struktur-Deklaration:

struct datum
{
  int tag;
  char monat[10];
  int jahr;
} geburtstag, urlaub;

Die zweite Möglichkeit besteht darin, die Struktur zunächst wie oben zu deklarieren und Variablen der Struktur später zu erzeugen:

struct datum geburtstag, urlaub;

Die Größe einer Variable vom Typ struct datum kann mit sizeof(struct datum) ermittelt werden. Die Gesamtgröße eines struct-Typs kann mehr sein als die Größe der einzelnen Komponenten, in unserem Fall also sizeof(int) + sizeof(char[10]) + sizeof(int). Der Compiler darf nämlich die einzelnen Komponenten so im Speicher ausrichten, dass ein schneller Zugriff möglich ist. Beispiel:

 struct Test
 {
  char c;
  int i;
 };

 sizeof(struct Test); // Ergibt wahrscheinlich nicht 5

Der Compiler wird wahrscheinlich mehr Bytes als die Summe der Einzelkomponenten reservieren:
Online-Compiler ideone:

#include <stdio.h>
#include <stddef.h>

struct Test
{
  char c;
  int i;
};

int main(void)
{
	printf("Groesse : %lu\n", (unsigned long)sizeof(struct Test));
	printf("Offset c: %lu\n", (unsigned long)offsetof(struct Test,c));
	printf("Offset i: %lu\n", (unsigned long)offsetof(struct Test,i));
	return 0;
}
Groesse : 8
Offset c: 0
Offset i: 4


Die Zuweisung kann komponentenweise erfolgen und eine Initialisierung erfolgt immer mit geschweifter Klammer:

  struct datum geburtstag = {7, "Mai", 2005};

Beim Zugriff auf eine Strukturvariable muss immer der Bezeichner der Struktur durch einen Punkt getrennt mit angegeben werden. Mit

 geburtstag.jahr = 1964;

wird der Komponente jahr der Struktur geburtstag der neue Wert 1964 zugewiesen.

Der gesamte Inhalt einer Struktur kann einer anderen Struktur zugewiesen werden. Mit

  urlaub = geburtstag;

wird der gesamte Inhalt der Struktur geburtstag dem Inhalt der Struktur urlaub zugewiesen.

Es gibt auch Zeiger auf Strukturen. Mit

 struct datum *urlaub;

wird urlaub als ein Zeiger auf eine Variable vom Typ struct datum vereinbart. Der Zugriff auf das Element tag erfolgt über (*urlaub).tag.

Die Klammern sind nötig, da der Vorrang des Punktoperators höher ist als der des Dereferenzierungsoperators *. Würde die Klammer fehlen, würde der Dereferenzierungsoperator auf den gesamten Ausdruck angewendet, so dass man stattdessen *(urlaub.tag) erhalten würde. Da die Komponente tag aber kein Zeiger ist, würde man hier einen Fehler erhalten.

Da Zeiger auf Strukturen sehr häufig gebraucht werden, wurde in C der ->-Operator (auch Strukturoperator genannt)eingeführt. Er steht an der Stelle des Punktoperators. So ist beispielsweise (*urlaub).tag äquivalent zu urlaub->tag.

Unions

Unions sind Strukturen sehr ähnlich. Der Hauptunterschied zwischen Strukturen und Unions liegt allerdings darin, dass die Elemente denselben Speicherplatz bezeichnen. Deshalb benötigt eine Variable vom Typ union nur genau soviel Speicherplatz, wie ihr jeweils größtes Element.

Unions werden immer da verwendet, wo man komplexe Daten interpretieren will. Zum Beispiel beim Lesen von Datendateien. Man hat sich ein bestimmtes Datenformat ausgedacht, weiß aber erst beim Interpretieren, was man mit den Daten anfängt. Dann kann man mit den Unions alle denkbaren Fälle deklarieren und je nach Kontext auf die Daten zugreifen. Eine andere Anwendung ist die Konvertierung von Daten. Man legt zwei Datentypen "übereinander" und kann auf die einzelnen Teile zugreifen.

Im folgenden Beispiel wird ein char-Element mit einem short-Element überlagert. Das char-Element belegt genau 1 Byte, während das short-Element 2 Byte belegt.

Beispiel:

 union zahl
 {
   char  c_zahl; //1 Byte
   short s_zahl; //1 Byte + 1 Byte
 }z;

Mit

 z.c_zahl = 5;

wird dem Element c_zahl der Variable z der Wert 5 zugwiesen. Da sich c_zahl und das erste Byte von s_zahl auf derselben Speicheradresse befinden, werden nur die 8 Bit des Elements c_zahl verändert. Die nächsten 8 Bit, welche benötigt werden, wenn Daten vom Typ short in die Variable z geschrieben werden, bleiben unverändert. Wird nun versucht auf ein Element zuzugreifen, dessen Typ sich vom Typ des Elements unterscheidet, auf das zuletzt geschrieben wurde, ist das Ergebnis nicht immer definiert.

Wie auch bei Strukturen kann der -> Operator auf eine Variable vom Typ Union angewendet werden.

Unions und Strukturen können beinahe beliebig ineinander verschachtelt werden. Eine Union kann also innerhalb einer Struktur definiert werden und umgekehrt.

Beispiel:

 union vector3d {
  struct { float x, y, z; } vec1;
  struct { float alpha, beta, gamma; } vec2;
  float vec3[3];
 };

Um den in der Union aktuell verwendeten Datentyp zu erkennen bzw. zu speichern, bietet es sich an, eine Struktur zu definieren, die die verwendete Union zusammen mit einer weiteren Variable umschliesst. Diese weitere Variable kann dann entsprechend kodiert werden, um den verwendeten Typ abzubilden:

 struct checkedUnion {
  int type;  // Variable zum Speichern des in der Union verwendeten Datentyps
  union intFloat {
   int i;
   float f;
  } intFloat1;
 };

Wenn man jetzt eine Variable vom Typ struct checkedUnion deklariert, kann man bei jedem Lese- bzw. Speicherzugriff den gespeicherten Datentyp abprüfen bzw. ändern. Um nicht direkt mit Zahlenwerten für die verschiedenen Typen zu arbeiten, kann man sich Konstanten definieren, mit denen man dann bequem arbeiten kann. So könnte der Code zum Abfragen und Speichern von Werten aussehen:

#include <stdio.h>

#define UNDEF 0
#define INT 1
#define FLOAT 2

struct checkedUnion {
  int type;
  union intFloat {
   int i;
   float f;
  } intFloat1;
};

int main(void)
{
 struct checkedUnion test1;
 test1.type = UNDEF; // Initialisierung von type mit UNDEF=0, damit der undefinierte Fall zu erkennen ist
 int testInt = 10;
 float testFloat = 0.1;

 /* Beispiel für einen Integer */
 test1.type = INT; // setzen des Datentyps für die Union
 test1.intFloat1.i = testInt; // setzen des Wertes der Union

 /* Beispiel für einen Float */
 test1.type = FLOAT;
 test1.intFloat1.f = testFloat;

 /* Beispiel für einen Lesezugriff */
 if (test1.type == INT) {
  printf ("Der Integerwert der Union ist: %d\n", test1.intFloat1.i);
 } else if (test1.type == FLOAT) {
  printf ("Der Floatwert der Union ist: %lf\n", test1.intFloat1.f);
 } else {
  printf ("FEHLER!\n");
 }

 return 0;
}

Folgendes wäre also nicht möglich, da die von der Union umschlossene Struktur zwar definiert aber nicht deklariert wurde:

 union impossible {
  struct { int i, j; char l; }; // Deklaration fehlt, richtig wäre: struct { ... } structName;
  float b;
  void* buffer;
 };

Unions sind wann immer es möglich ist zu vermeiden. Type punning (engl.) – zu deutsch etwa spielen mit den Datentypen – ist eine sehr fehlerträchtige Angelegenheit und erschwert das Kompilieren auf anderen und die Interoperabilität mit anderen Systemen mitunter ungemein.

Aufzählungen

Die Definition eines Aufzählungsdatentyps (enum) hat die Form

 enum [Typname] {
     Bezeichner [= Wert] {, Bezeichner [= Wert]}
 };

Damit wird der Typ Typname definiert. Eine Variable diesen Typs kann einen der mit Bezeichner definierten Werte annehmen. Beispiel:

 enum Farbe {
     Blau, Gelb, Orange, Braun, Schwarz
 };

Aufzählungstypen sind eigentlich nichts anderes als eine Definition von vielen Konstanten. Durch die Zusammenfassung zu einem Aufzählungstyp wird ausgedrückt, dass die Konstanten miteinander verwandt sind. Ansonsten verhalten sich diese Konstanten ähnlich wie Integerzahlen, und die meisten Compiler stört es auch nicht, wenn man sie bunt durcheinander mischt, also zum Beispiel einer int-Variablen den Wert Schwarz zuweist.

Für Menschen ist es sehr hilfreich, Bezeichner statt Zahlen zu verwenden. So ist bei der Anweisung textfarbe(4) nicht gleich klar, welche Farbe denn zur 4 gehört. Benutzt man jedoch textfarbe(Schwarz), ist der Quelltext leichter lesbar.

Bei der Definition eines Aufzählungstyps wird dem ersten Bezeichner der Wert 0 zugewiesen, falls kein Wert explizit angegeben wird. Jeder weitere Bezeichner erhält den Wert seines Vorgängers, erhöht um 1. Beispiel:

 enum Primzahl {
     Zwei = 2, Drei, Fuenf = 5, Sieben = 7
 };

Die Drei hat keinen expliziten Wert bekommen. Der Vorgänger hat den Wert 2, daher wird Drei = 2 + 1 = 3.

Meistens ist es nicht wichtig, welcher Wert zu welchem Bezeichner gehört, Hauptsache sie sind alle unterschiedlich. Wenn man die Werte für die Bezeichner nicht selbst festlegt (so wie im Farbenbeispiel oben), kümmert sich der Compiler darum, dass jeder Bezeichner einen eindeutigen Wert bekommt. Aus diesem Grund sollte man mit dem expliziten Festlegen auch sparsam umgehen.

Variablen-Deklaration

Es ist zu beachten, dass z.B. Struktur-Variablen wie folgt deklariert werden müssen:

struct StrukturName VariablenName;

Dies kann umgangen werden, indem man die Struktur wie folgt definiert:

typedef struct
{
  // Struktur-Elemente
} StrukturName;

Dann können die Struktur-Variablen einfach durch

StrukturName VariablenName;

deklariert werden. Dies gilt nicht nur für Strukturen, sondern auch für Unions und Aufzählungen.

Folgendes ist auch möglich, da sowohl der Bezeichner struct StrukturName, wie auch StrukturName, definiert wird:

typedef struct StrukturName
{
   // Struktur-Elemente
} StrukturName;

StrukturName VariablenName1;
struct StrukturName VariablenName2;

Mit typedef können Typen erzeugt werden, ähnlich wie "int" und "char" welche sind. Dies ist hilfreich um seinen Code noch genauer zu strukturieren.

Beispiel:

typedef char name[200];
typedef char postleitzahl[5];

typedef struct {
	name strasse;
	unsigned int hausnummer;
	postleitzahl plz;
} adresse;

int main()
{
	name vorname, nachname;
	adresse meine_adresse;
}