Zusammengesetzte Datentypen

typedef – Synonyme für andere Datentypen

Mit dem Schlüsselwort typedef kann ein neuer Name für einen beliebigen Datentyp vergeben werden. Die Syntax lautet dabei wie folgt:

typedef datentyp neuer_datentyp

Beispielsweise kann mittels typedef int integer ein “neuer” Datentyp namens integer erzeugt werden. Dieser kann anschließend wie gewohnt bei Deklarationen von Variablen verwendet werden, beispielsweise wird durch integer num_1; eine neue Variable als Integer-Wert deklariert.

Die Verwendung von typedef ist insbesondere bei der Definition von zusammengesetzten Datentypen hilfreich.

enum – Aufzählungen

Aufzählungen (“enumerations”) bieten neben #define-Anweisungen eine einfache Möglichkeit, einzelnen Begriffen eine Nummer zuzuweisen und sie somit im Quellcode als leicht lesbare Bezeichner verwenden zu können.

Bei der Deklaration eines enum-Typs werden die einzelnen Elemente der Aufzählung durch Komma-Zeichen getrennt aufgelistet. Sie bekommen dabei, sofern nicht explizit andere Werte angegeben werden, automatisch die Nummern 0, 1, 2, ... zugewiesen; bei expliziten Wertzuweisungen wird der Wert für jedes folgende Element um 1 erhöht.

typedef enum
{
    const1, const2, const3, ...
} enum_name;

# Beispiel:

typedef enum
{
    MONTAG = 1, DIENSTAG, MITTWOCH, DONNERSTAG, FREITAG, SAMSTAG, SONNTAG
} wochentag;

Allgemein müssen die Elemente eines enum-Typs unterschiedliche Werte besitzen. Oftmals werden die aufgelisteten Elemente zudem in Großbuchstaben geschrieben, um hervorzuheben, dass es sich auch bei ihnen um (ganzzahlige) Konstante handelt.

Nach der obigen Deklaration ist beispielsweise wochentag als neuer Datentyp verfügbar, der stets durch einen “benannten” int-Wert repräsentiert wird:

wochentag heute = DIENSTAG;

// Die zugewiesene Nummer ausgeben:

printf("Heute ist der %d. Tag der Woche\n", heute);
// Ergebnis: Heute ist der 2. Tag der Woche.

// Funktionen definieren:

wochentag morgen(wochentag heute)
{
    if (heute == SONNTAG)
        return 1;
    else
        return heute++;
}

Es können somit nach der Deklaration des enum-Datentyps auch dessen Elemente als numerische Konstante im C-Code verwendet werden.

struct – Strukturen

Strukturen (“structs”) ermöglichen es in C mehrere Komponenten zu einer Einheit zusammenenzufassen, ohne dass diese den gleichen Datentyp haben müssen (wie es bei einem Array der Fall ist. Der Speicherplatzbedarf einer Struktur entspricht dabei dem Speicherplatzbedarf ihrer Komponenten. In dem meisten Fällen lassen sich Strukturen folgendermaßen definieren:

typedef struct
{
    // ... Deklaration der Komponenten ...

} struct_name;

// Beispiel:

typedef struct
{
    char name[50];
    int laenge;
    int breite;
    int hoehe;
} gegenstand;

Nach der Deklaration einer Struktur kann diese als neuer Datentyp verwendet werden. Die einzelnen Komponenten werden nicht dabei durchnummeriert, sondern lassen sich mittels des Strukturzugriff-Operators . über bei der Definition vergebene Schlüsselwörter ansprechen:

// Struktur-Objekt definieren:

gegenstand tisch =
{
    "Schreibtisch", 140, 60, 75
};

// Informationen zum Objekt ausgeben:

printf( "Der Gegenstand \"%s\" ist %d cm hoch.\n", tisch.name, tisch.hoehe );
// Ergebnis: Der Gegenstand "Schreibtisch" ist 75 cm hoch.

Handelt es sich bei einer Struktur-Komponente um einen Zeiger, beispielsweise eine Zeichenkette, so muss der Inhalts-Operator * vor den Strukturnamen geschrieben werden. Im obigen Beispiel würde man also nicht tisch.*name schreiben (was beim Compilieren einen Fehler verursachen würde), sondern *tisch.name, da der Strukturzugriff-Operator . eine höhere Priorität besitzt. Zuerst wird also der Strukturzugriff ausgewertet, wobei sich eine Variable vom Typ char * ergibt; anschließend kann diese mit dem Inhaltsoperator dereferenziert werden. Bei *strukturname.komponente kann somit der Punkt wie ein Teil des Veriablennamens gelesen werden.

Strukturen können andere Strukturen als Komponenten enthalten; rekursive Strukturen, die sich selbst als Komponente beinhalten, sind nicht möglich. Eine Struktur kann allerdings einen Zeiger auf sich selbst enthält, so dass beispielsweise so genannte Verkettungen möglich sind. Darauf wird im Abschnitt Dynamische Datenstrukturen näher eingegangen.

Zeiger auf Strukturen

Eine Struktur wird selten direkt als Argument an eine Funktion übergeben, da hierbei der gesamte Strukturinhalt kopiert werden müsste. Stattdessen wird üblicherweise ein Zeiger auf die Struktur an die Funktion übergeben.

Hat man beispielsweise eine Struktur mystruct mit den Komponenten int a und int b und ein bereits existierendes mystruct-Objekt x_1, so kann man mittels mystruct * x_1_pointer = &x_1; einen Zeiger auf die Struktur definieren. Mittels eines solchen Pointers kann man auf folgende Weise auf die Komponenten der Struktur zugreifen:

// Struktur deklarieren:
typedef struct
{
    int a;
    int b;
} mystruct;

// Struktur-Objekt erzeugen:
mystruct x = {3, 5};

// Pointer auf Struktur-Objekt erzeugen:
mystruct * xpointer = &x;

// Wertzuweisung mittels Pointer:
(*xpointer).a = 5;

Im obigen Beispiel sind die Klammmern um *x_1_pointer notwendig, da der Strukturzugriff-Operator . eine höhere Priorität hat als der Inhalts-Operator *. Da Strukturen und somit auch Zeiger auf Strukturen sehr häufig vorkommen und diese Schreibweise etwas umständlich ist, gibt es in C folgende Kurzschreibweise:

(*xpointer).a == xpointer->a
// Ergebnis: TRUE

Mit dem Pfeil-Operator -> kann also in gleicher Weise auf die Komponenten eines Struktur-Pointers zugegriffen werden wie mit . auf die Komponennten der Struktur selbst.

union – Alternativen

Mittels des Schlüsselworts union lässt sich ein zusammengesetzter Datentyp definieren, bei dem sich die bei der Deklaration angegebenen Elemente einen gemeinsamen Speicherplatz teilen: Es kann dabei zu jedem Zeitpunkt nur eine der angegebenen Komponenten aktiv sein. Der Speicherplatzbedarf einer Union entspricht somit dem Speicherplatzbedarf der größten angegebenen Komponente. Die Deklaration einer union erfolgt nach folgendem Schema:

typedef union
{
    // ... Deklaration der Komponenten ...

} union_name;

// Beispiel:

typedef union
{
    char text[20];
    int ganzzahl;
    float kommazahl;
} cell_value;

Nach der Deklaration einer Union kann diese als neuer Datentyp verwendet werden. Der Zugriff auf die einzelnen möglichen Elemente, die eine Union-Variable beinhaltet, erfolgt wie bei Strukturen, mit dem .-Operator:

// Union-Variablen deklarieren:

cell_value cell_1 = {"Hallo Welt!"};
cell_value cell_2 = {42};
cell_value cell_3 = {2.35813};

// Auf Inhalt einer Union zugreifen:

printf("%s\n", cell_1.text)

Im Falle eines Zeigers auf eine union-Variable kann, ebenso wie bei Zeigern auf Strukturen, mit dem Pfeil-Operator -> auf die einzelnen Komponenten zugegriffen werden.

Unabhängig davon, welche Komponente aktuell in einer union-Variable mit einem Wert versehen ist, können stets alle möglichen Komponenten der Union abgefragt werden; dabei wird der aktuell gespeicherte Wert mittels eines automatischen Casts in den jeweiligen Datentyp umgewandelt. Da diese Umwandlung zu unerwarteten Ergebnissen führen kann, kann es hilfreich sein, für die einzelnen Datentypen der Union-Komponenten symbolische Konstanten zu vergeben. Fasst man dann sowohl den aktuellen Typ der Union-Variablen sowie die Union-Variable zu einer Struktur zusammen, so lässt sich bei komplexeren Datentypen nicht nur Speicherplatz sparen, es kann auch mittels einer case-Anweisung gezielt Code in Abhängigkeit vom aktuellen Wert aufgerufen werden:

typedef enum
{
    STRING=0, INTEGER=1, FLOAT=2
} u_type;

typedef struct
{
    u_type type;
    cell_value value;
} cell_content;

cell_content my_cell;

my_cell.type = FLOAT;
my_cell.value = 3.14;

switch (my_cell.type)
{
    case STRING:
        printf("In dieser Zelle ist die Zeichenkette %s gespeichert.", *my_cell.value);

    case INT:
        printf("In dieser Zelle ist die int-Zahl %d gespeichert.", my_cell.value);

    case FLOAT:
        printf("In dieser Zelle ist die float-Zahl %f gespeichert.", my_cell.value);
}

Auf diese Weise könnte in einem “echten” Programm die Ausgaben eines Wertes aufgrund nicht nur seines Datentyps, sondern beispielsweise auch aufgrund von Darstellungsoptionen (Anzahl an Kommastellen, Prozentwert, usw.) angepasst werden.