Eine Struktur sagt „all das, zusammen". Eine union sagt „genau eines davon, zur Zeit". Die Elemente liegen übereinander an derselben Adresse, die Union ist also nur so groß wie ihr größtes Element, und ein Element zu schreiben zerstört die anderen.
Das klingt nach einer Falle, und ungeprüft ist es das. Aber es ist auch die Art, wie C einen Wert ausdrückt, der von mehreren Arten sein könnte - ein Token, das eine Zahl oder eine Zeichenkette ist, eine Nachricht, die ein Klick oder ein Tastendruck ist -, ohne für alle gleichzeitig zu bezahlen.
Eine Union deklarieren und benutzen
Die Syntax spiegelt eine Struktur exakt; nur das Schlüsselwort ändert sich.
Auf Elemente wird mit . (oder -> über einen Zeiger) zugegriffen, genau wie bei einer Struktur. Anders ist, dass nur das zuletzt geschriebene Element einen bedeutsamen Wert hält. Nach v.f = 3.5f liefert das Lesen von v.i nicht 3 - es liefert die Ganzzahl, die das Bitmuster von 3.5f zufällig buchstabiert.
Größe: das größte Element gewinnt
Vergleiche das Speicherlayout einer Struktur und einer Union mit denselben Elementen:
Auf einer typischen Maschine hat die Struktur 24 Bytes (4 für das int, 8 für das double, 1 für das char, plus Füllbytes), während die Union 8 hat - die Größe ihres double, zur Ausrichtung aufgerundet. Die drei ausgegebenen Adressen sind identisch, und das ist die ganze Geschichte der Unions in einer Ausgabezeile.
Beachte die %p-Casts nach void *. printf erwartet für %p genau das; einen anderen Zeigertyp zu übergeben ist undefiniertes Verhalten, auch wenn es meist zu funktionieren scheint. Siehe Formatangaben.
Eine Union initialisieren
Ein geklammerter Initialisierer ohne Benennung initialisiert das erste Element:
Die benannte Form ist die, die man nehmen sollte. {42} hängt still von der Elementreihenfolge ab, die Deklaration später umzusortieren ändert also, welches Element initialisiert wird - ein wirklich fieser Bug, weil am Code nichts anders aussieht.
Das eigentliche Problem: welches Element ist gültig?
Eine Union hält nicht fest, welches Element du zuletzt geschrieben hast. Es sind nur Bytes; das Wissen lebt in deinem Kopf, und genau dort geht Wissen verloren.
Die ausgegebene Zahl ist groß und seltsam - das Bitmuster von 1.0f, als int gelesen. Nichts stürzte ab, nichts warnte, und das Programm ist still falsch. Die Union tat genau, was sie versprach; der Fehler war unserer, zu vergessen, welches Element gültig war.
Die Lösung: eine markierte Union
Die Standardlösung ist, die Union mit einem enum zu paaren, das das gültige Element festhält, und beides in eine Struktur zu wickeln. Diese Kombination heißt markierte Union (oder diskriminierte Union), und so solltest du im Anwendungscode im Wesentlichen jede Union schreiben.
Jeder Lesezugriff geht nun über das switch auf kind, es ist also unmöglich, ein Element zu lesen, das nie geschrieben wurde - solange jeder Schreibzugriff auch die Markierung setzt. Die Schreibzugriffe in kleine Konstruktorfunktionen zu wickeln (value_from_int, value_from_string) ist der übliche Weg, das unvergesslich zu machen.
Die Speicherersparnis ist real: Jedes Value kostet hier 24 Bytes Nutzlast plus die Markierung statt 4 + 4 + 24 für eine Struktur, die alle drei hält. Bei hunderttausend davon zählt das.
Mit -Wall zu kompilieren fügt ein zweites Sicherheitsnetz hinzu: Fügst du dem enum später ein VAL_BOOL hinzu und vergisst einen case dafür, warnt GCC vor dem unbehandelten Aufzählungswert.
Anonyme Unions
C11 erlaubt ein Union-Element ohne Namen innerhalb einer Struktur und befördert dessen Elemente in den Namensraum der äußeren Struktur:
Weil die Union selbst unbenannt ist, schreibst du s->circle.r statt s->as.circle.r. Kürzer zu lesen, um den Preis zu verbergen, dass überhaupt eine Union im Spiel ist - was in Ordnung ist, wenn die Markierung direkt daneben steht.
Wo Unions sich wirklich bezahlt machen
Vier wiederkehrende Verwendungen:
- Variantenwerte. Interpreter, JSON- und Konfigurationsleser und Nachrichtenwarteschlangen tragen alle Werte, deren Typ zur Laufzeit entschieden wird. Eine markierte Union ist die kanonische Darstellung.
- Speicherknappe Datensätze. Wenn eine Struktur mehrere sich ausschließende Felder hat und du Millionen davon hast, ist sie zu überlagern eine direkte Ersparnis.
- Protokoll- und Hardwarelayouts. Ein Paket, dessen Nutzlast von einem Kopfbyte abhängt, bildet sich natürlich auf eine markierte Union ab, ebenso Geräteregister.
- Byte-Inspektion. Einen Wert mit einem
unsigned char[]-Array zu überlagern lässt dich seine einzelnen Bytes ansehen, zum Beispiel um die Byte-Reihenfolge zu bestimmen:
Das ist Type Punning - Bytes absichtlich als einen anderen Typ zurückzulesen. Über eine Union wie diese zu lesen ist in C ausdrücklich als implementierungsabhängiges Ergebnis erlaubt (anders als Zeiger zwischen unverwandten Typen zu casten, was die Aliasing-Regeln bricht), und Bytes als unsigned char zu betrachten ist immer sicher. Ein int als float umzudeuten ist eine andere Sache: Das Ergebnis hängt vollständig von der Darstellung deiner Plattform ab, halte es also aus portablem Code heraus.
Häufige Fehler
- Ein Element lesen, das du nicht geschrieben hast. Die Hauptgefahr. Nimm eine Markierung.
- Annehmen, eine Union wandle um. Tut sie nicht.
u.i = 3; float f = u.f;deutet Bits um;int i = 3; float f = i;wandelt um. Siehe Typumwandlung. - Einen Zeiger in eine Union legen und ihn aus den Augen verlieren. Hält ein Zweig ein
char *, das du belegt hast, verliert das Überschreiben der Union mit einem anderen Element ihn - nichts zeigt mehr auf den Puffer. Gib frei, bevor du den Zweig wechselst. - Erwarten, dass der Compiler prüft. Er tut es nicht. Unions sind eines der wenigen Sprachmittel in C, bei denen die Sprache außer Größe und Ausrichtung überhaupt keine Hilfe bietet; die Markierung ist deine einzige Leitplanke.
Häufig gestellte Fragen
Was ist eine Union in C?
Eine Union ist ein Typ, dessen Elemente sich alle denselben Speicher teilen. Ein Element zu schreiben überschreibt die anderen, eine Union hält also immer genau eines ihrer Elemente. Sie wird wie eine Struktur deklariert, aber mit dem Schlüsselwort union: union Value { int i; float f; };.
Was ist der Unterschied zwischen einer Union und einer Struktur in C?
Eine Struktur gibt jedem Element eigenen Speicher und hält alle gleichzeitig, ihre Größe ist also mindestens die Summe ihrer Elemente. Eine Union legt jedes Element an dieselbe Adresse und hält eines zur Zeit, ihre Größe ist also die ihres größten Elements.
Wie groß ist eine Union in C?
Groß genug für ihr größtes Element, zur Ausrichtung aufgerundet. Eine Union aus einem int (4 Bytes) und einem double (8 Bytes) hat 8 Bytes - nicht 12. sizeof ist der Weg, es auf deiner eigenen Plattform zu prüfen.
Was passiert, wenn man ein Union-Element liest, das man nicht geschrieben hat?
Du deutest dieselben Bytes als einen anderen Typ. u.i = 1 zu schreiben und dann u.f zu lesen wandelt nicht um - es liest das Bitmuster der Ganzzahl als Gleitkommazahl und ergibt eine bedeutungslose Zahl. In Standard-C ist das bestenfalls unspezifiziert, weshalb es das Muster der markierten Union gibt: Halte daneben eine Markierung, die sagt, welches Element gültig ist.