Struct und Union
Video zum Kapitel

Strukturen
Mit Arrays kann ich nur Daten eines Typs speichern und über einen Index darauf zugreifen. Wenn es darum geht, verschiedene Daten in einer strukturierten Form abzulegen oder aber über einen Namen zu adressieren, bieten sich in C der Datentyp struct
an.
- Strukturen (structs) sind ähnlich zu Java Objekten (nur ohne Methoden)
- jeder andere Typ kann Komponente einer Struktur sein
- Zugriff mit
struct.field
- Werden auf dem Stack angelegt (↔ Java-Klassen: Heap)
struct { int x; int y; float w; } rect;
rect.x = 10;
rect.y = 20;
rect.w = 2.5;
Strukturen, die wie im Beispiel oben, deklariert wurde, werden – anders als in Java – auf dem Stack abgelegt und sind somit nur in der aktuellen Funktion benutzbar. Will man die Struktur auf dem Heap ablegen, muss man sie dynamisch erzeugen (siehe unten). Der Ausdruck struct { ... } NAME;
alloziert bereits den Speicher auf dem Stack, ist also nicht nur eine Deklaration, sondern auch direkt eine Definition der Variable NAME
.
Wenn man von Java kommt, kann man sich Strukturen wie Java-Klassen vorstellen, bei denen alle Attribute public
sind und keine Methoden existieren.
// Dies ist nur eine Deklaration!
class Rect {
int x;
int y;
float w;
}
class Main {
public static void main(String args[]) {
// Hier wird das Objekt erst angelegt.
Rect rect = new Rect();
rect.x = 10;
rect.y = 20;
rect.w = 2.5f;
}
}
Die syntaktische Besonderheit, dass bei einer Struktur in C die Deklaration und Definition direkt zusammenfallen passt nicht zu modernen Programmieransätzen.
Deswegen kann man eine Struktur in C deklarieren und dann an späteren Stellen wieder referenzieren, ohne direkt eine Variable anzulegen. Dies erfolgt mit der Syntax struct NAME {...};
. NAME
ist in diesem Fall keine Variable, sondern ein sogenannter Tag, der die Struktur bezeichnet. Bei der Variablendeklaration muss man allerdings das struct
wiederholen. Einen Ausweg schafft typedef
.
Will man eine Struktur-Definition wiederverwenden
- Struktur benennen →
struct
muss in der Variablendeklaration wiederholt werden typedef
verwenden →struct
muss nicht wiederholt werden
struct Complex { double real; double imag; };
struct Point { double x; double y; } corner;
struct Point p;
p.x = 7.0; corner.x = 9.0;
Diese Variante ist nicht besonders modern und man sollte heutzutage grundsätzlich auf ein typedef
zurückgreifen.
typedef struct { double real; double imag; } Complex;
typedef struct { double x; double y; } Point;
Point p;
Complex z;
p.x = 7.0; z.real = 9.8;
Beachten Sie, dass beim typedef
der Name der Struktur hinten steht, bei der Variante ohne typedef
aber vorne.
Tags bilden einen eigenen Namensraum, der vom Namensraum der Variablen getrennt ist. Im Beispiel struct S { int a };
heißt der Datentyp der Struktur nicht S
, sondern struct S
. Der Name S
kann für Variablen, Enumerationen oder Unions wiederverwendet werden.
Für die Initialisierung von Strukturen gibt es eine verkürzte Syntax, welche die Initialisierung direkt bei der Definition durchführt.
Initialisierung von Strukturen
typedef struct { double x; double y; } Point;
Point p1;
p1.x = 4.2;
p1.y = 2.3;
Point p2 = { 4.2, 2.3 };
Point p3 = { .y = 2.3, .x = 4.2 };
Point p4 = { .x = p3.x, .y = p3.y };
Wenn man die Elemente der Struktur bei der Initialisierung benennt, dann ist die Reihenfolge egal und er Code ist robuster gegenüber späteren Erweiterungen der Struktur.
Die Werte bei der Initialisierung der Struktur müssen keine Konstanten sein.
Da Strukturen sehr häufig dynamisch alloziert werden, verwaltet man sie über Pointer. Die Syntax für das Dereferenzieren der Pointer ist aber im Zusammenhang mit Strukturen sehr sperrig (*struct_pointer).element
. Deswegen gibt es eine syntaktische Vereinfachung.
- Strukturen werden häufig mit Pointern verwendet (dynamische Erzeugung mit
malloc
) - Kurzform für das Dereferenzieren des Pointers mit
->
(*sp).element = 42;
y = (*sp).element;
sp->element = 42;
y = sp->element;
Größe einer Struktur
Für die dynamische Speicherverwaltung ist es wichtig zu wissen, wie viel Speicher eine Struktur verbraucht. Eine Struktur ist natürlich mindestens so groß, wie sie Summe der darin benutzten Datentypen. Allerdings kann sie auch mehr Speicher verbrauchen, und zwar dann, wenn der Compiler Padding einfügt.
- Die Größe einer Struktur ergibt sich aus der Größe der Elemente + Padding für Alignment der Adressen
struct {
char x;
int y;
char z;
} s1;
printf("%ld\n", sizeof(s1)); /* -> 12 */
struct {
char x, z;
int y;
} s2;
printf("%ld\n", sizeof(s2)); /* -> 8 */
Wie man sieht, verbrauchen die Strukturen unterschiedlich viel Speicher, obwohl sie genau dieselben Daten tragen. Der Grund ist das Padding, das der Compiler eingefügt hat, um die Adressen der Objekte zu alignen (auszurichten). Damit ist gemeint, dass die Adressen ganzzahlige Vielfache von einer Konstante (normalerweise 2, 4 oder 8) sind. Das Alignment beschleunigt den Zugriff auf die Daten durch den Prozessor, weil dieser nur ausgerichtete Adressen mit maximaler Geschwindigkeit lesen kann.
Im Beispiel scheint der Compiler ein Alignment auf Adressen durchzuführen, die durch vier teilbar sind. Damit sieht die erste Struktur (aus Sicht des Compilers) wie folgte aus:
struct {
char x; /* Offset: 0 */
char _padding[3]; /* Offset: 1 */
int y; /* Offset: 4 */
char z; /* Offset: 8 */
char _padding[3]; /* Offset: 9 */
} s1;
Das Padding am Ende der Struktur dient dazu, dass die Gesamtgröße der Struktur ebenfalls ein Vielfaches des Alignments ist. Ohne das Padding wäre die Struktur 9 Bytes groß, sodass sie auf 12 Bytes aufgefüllt wird, damit ihre Größe wieder durch vier teilbar wird.
Bei der zweiten Struktur ist weniger Padding erforderlich.
struct {
char x; /* Offset: 0 */
char z; /* Offset: 1 */
char _padding[2]; /* Offset: 2 */
int y; /* Offset: 4 */
} s2;
Struktur dynamisch anlegen
Ebenso wie Arrays möchte man Strukturen dynamisch verwalten können. Dies erfolgt dann analog mit malloc()
und free()
.
typedef struct { int x; int y; } Point2D;
/* Speicher allozieren */
Point2D *p = (Point2D*) malloc(sizeof(Point2D));
p->x = 12;
p->y = 23;
printf("x=%d, y=%d\n", p->x, p->y); /* x=12, y=23 */
/* Speicher freigeben */
free(p);
Die Verwendung des ->
-Operators erspart einem beim Umgang mit Pointer auf Strukturen das ständige dereferenzieren mit *
.
Unions
Ein absoluter Exot unter den Datentypen ist die Union, die man aus Java und anderen Sprachen nicht kennt. Laut den C-Erfindern benutzt man sie dann, wenn man eine Variable benötigt, die zu unterschiedlichen Zeiten unterschiedliche Typen aufnehmen soll aber immer dieselbe Größe haben soll.
- Unions sind ähnlich zu Strukturen, speichern aber immer nur einen Wert
- Die Größe entspricht dem größten Datentyp
union u_tag {
int ival;
float fval;
char *sval;
} u;
printf("sizeof=%ld\n", sizeof(u)); /* sizeof=8 */
u.ival = 12;
printf("u.ival=%d\n", u.ival); /* u.ival=12 */
u.fval = 8.0;
printf("u.fval=%f\n", u.fval); /* u.fval=8.0 */
printf("u.ival=%d\n", u.ival); /* u.ival=1090519040 */
Die Größe der Union im Beispiel ergibt sich aus dem größten Datentyp, der in diesem Fall char*
ist, weil ein Pointer auf einer 64-Bit-Plattform 8 Byte benötigt.
Die letzte Zeile im Beispiel zeigt, das man wissen muss, welche Daten man in die Union geschrieben hat, da ein Zugriff über den falschen Typ zu seltsamen Ergebnissen führen kann. Das Ergebnis ergibt sich aus dem Bitmuster, die der float
-Wert 8.0
hat, wenn man es als Integer interpretiert.
Da wir heute meistens genügend Speicher zur Verfügung haben, ist der Spareffekt der Union nicht mehr relevant.
Bitfelder
Mit Bitfeldern kann man in C Variablen definieren, die eine bestimmte Anzahl von Bits haben. Die einzelnen Bits können benannt werden.
- Ein Bitfeld (bitfield) ist eine Integer-Variable mit einer bestimmten Anzahl von Bits
Syntax:TYP [NAME] : BREITE
- Bits können über Namen angesprochen werden
- Der Compiler packt die Daten aufeinanderfolgender Bitfelder eng zusammen
struct Date {
unsigned int day : 5;
unsigned int month : 4;
signed int year : 22;
char isDST : 1;
};
Dieses Beispiel zeigte die Verwendung eines 32 Bit breiten Bitfeldes, um das aktuelle Datum in einem einzigen Integer zu speichern.
#include <stdio.h>
typedef struct {
unsigned int day : 5;
unsigned int month : 4;
signed int year : 22;
char isDST : 1;
} Date;
int main(int argc, char** argv) {
Date myDate = { 24, 12, 2017, 0 };
printf("%ld\n", sizeof(myDate)); /* -> 4 */
printf("%d-%d-%d\n", myDate.year, myDate.month, myDate.day);
/* -> 2017-12-24 */
}
Bitmasken
Bitfelder vereinfachen den Zugriff auf die einzelnen Bits eines Wertes und werden z. B. eingesetzt, um Protokolle zu implementieren. Eine Alternative dazu ist, die Bits selbst zu setzen und zu verwalten.
Alternativ zu Bitfeldern kann man die Daten auch selbst packen und entpacken
#include <stdio.h>
#include <stdint.h>
#define DAY_SHIFT 27
#define MONTH_SHIFT 23
#define YEAR_SHIFT 1
#define DAY_MASK 0x1f
#define MONTH_MASK 0x0f
#define YEAR_MASK 0x3fffff
#define DST_MASK 0x01
uint32_t encode(int day, int month, int year) {
return (((day & DAY_MASK) << DAY_SHIFT)
| ((month & MONTH_MASK) << MONTH_SHIFT)
| ((year & YEAR_MASK) << YEAR_SHIFT));
}
void decode(uint32_t date, int *day, int *month, int *year) {
*day = (date >> DAY_SHIFT) & DAY_MASK;
*month = (date >> MONTH_SHIFT) & MONTH_MASK;
*year = (date >> YEAR_SHIFT) & YEAR_MASK;
}
int main(int argc, char** argv) {
uint32_t myDate;
int day, month, year;
myDate = encode(24, 12, 2017);
printf("%u\n", myDate); /* 3321892802 */
myDate = 763367326;
decode(myDate, &day, &month, &year);
printf("%d-%d-%d\n", year, month, day); /* 1999-11-5 */
}