Struktura mówi "wszystkie te pola, razem". union mówi "dokładnie jedno z nich naraz". Pola są nałożone na siebie pod tym samym adresem, więc unia jest tak duża jak jej największe pole, a zapis jednego pola niszczy pozostałe.
Brzmi to jak pułapka i bez kontroli rzeczywiście nią jest. Ale to także sposób, w jaki C wyraża wartość, która może być jednego z kilku rodzajów, na przykład token będący liczbą albo stringiem czy komunikat będący kliknięciem albo naciśnięciem klawisza, bez płacenia za wszystkie naraz.
Deklaracja i użycie unii
Składnia jest dokładnie taka jak w strukturze; zmienia się tylko słowo kluczowe.
Do pól odwołujesz się przez . (albo -> przez wskaźnik), dokładnie jak w strukturze. Różnica polega na tym, że sensowną wartość ma tylko pole zapisane jako ostatnie. Po v.f = 3.5f odczyt v.i nie da ci 3, tylko liczbę całkowitą, którą akurat tworzy wzorzec bitowy 3.5f.
Rozmiar: wygrywa największe pole
Porównaj układ w pamięci struktury i unii o tych samych polach:
Na typowej maszynie struktura zajmuje 24 bajty (4 na int, 8 na double, 1 na char plus wypełnienie), a unia 8, czyli rozmiar jej double zaokrąglony ze względu na wyrównanie. Trzy wypisane adresy są identyczne i to jest cała historia unii w jednej linii wyjścia.
Zwróć uwagę na rzutowanie na void * przy %p. printf oczekuje dla %p dokładnie tego typu; przekazanie innego typu wskaźnika to niezdefiniowane zachowanie, nawet jeśli zwykle wydaje się działać. Zobacz specyfikatory formatu.
Inicjalizacja unii
Inicjalizator w nawiasach klamrowych bez desygnatora inicjalizuje pierwsze pole:
Forma z desygnatorem to ta, której warto używać. {42} po cichu zależy od kolejności pól, więc późniejsza zmiana kolejności w deklaracji zmienia, które pole zostanie zainicjalizowane: naprawdę paskudny błąd, bo w kodzie nic nie wygląda inaczej.
Prawdziwy problem: które pole jest aktualne?
Unia nie zapamiętuje, które pole zapisano jako ostatnie. To tylko bajty; ta wiedza jest w twojej głowie, a właśnie tam wiedza się gubi.
Wypisana liczba jest duża i dziwna: to wzorzec bitowy 1.0f odczytany jako int. Nic się nie wysypało, nic nie ostrzegło, a program po cichu działa źle. Unia zrobiła dokładnie to, co obiecywała; błędem było zapomnienie, które pole jest aktualne.
Rozwiązanie: unia z etykietą
Standardowe rozwiązanie to połączenie unii z enumem, który zapisuje aktualne pole, i opakowanie obu w strukturę. Takie połączenie nazywa się unią z etykietą (tagged union, czyli unia dyskryminowana) i praktycznie każdą unię w kodzie aplikacji warto pisać właśnie tak.
Każdy odczyt przechodzi teraz przez switch na kind, więc nie da się odczytać pola, którego nigdy nie zapisano, o ile każdy zapis ustawia też etykietę. Zwykle, żeby nie dało się o tym zapomnieć, opakowuje się zapisy w małe funkcje konstruujące (value_from_int, value_from_string).
Oszczędność pamięci jest realna: każda Value kosztuje tutaj 24 bajty ładunku plus etykietę, zamiast 4 + 4 + 24 w strukturze przechowującej wszystkie trzy pola. Przy stu tysiącach takich wartości ma to znaczenie.
Kompilacja z -Wall daje drugą siatkę bezpieczeństwa: jeśli później dodasz do enuma VAL_BOOL i zapomnisz o case dla niego, GCC ostrzeże o nieobsłużonej wartości wyliczenia.
Unie anonimowe
C11 pozwala umieścić w strukturze pole typu unia bez nazwy, co przenosi pola unii do przestrzeni nazw zewnętrznej struktury:
Ponieważ sama unia nie ma nazwy, piszesz s->circle.r zamiast s->as.circle.r. Krócej się to czyta, kosztem ukrycia, że w ogóle jest tu unia, co jest w porządku, gdy etykieta leży tuż obok.
Gdzie unie naprawdę się przydają
Cztery powtarzające się zastosowania:
- Wartości wariantowe. Interpretery, parsery JSON i plików konfiguracyjnych oraz kolejki komunikatów przenoszą wartości, których typ ustala się w czasie działania programu. Unia z etykietą to ich kanoniczna reprezentacja.
- Oszczędne pamięciowo rekordy. Gdy struktura ma kilka wzajemnie wykluczających się pól, a ty masz miliony takich struktur, nałożenie tych pól to bezpośrednia oszczędność.
- Układy protokołów i sprzętu. Pakiet, którego ładunek zależy od bajtu nagłówka, naturalnie odwzorowuje się na unię z etykietą, podobnie jak rejestry urządzeń.
- Podgląd bajtów. Nałożenie wartości na tablicę
unsigned charpozwala obejrzeć jej poszczególne bajty, na przykład żeby ustalić kolejność bajtów (endianness):
To jest type punning, czyli celowe odczytanie bajtów jako innego typu. Odczyt przez unię w ten sposób jest w C wprost dozwolony i daje wynik zależny od implementacji (w przeciwieństwie do rzutowania wskaźników między niezwiązanymi typami, które łamie reguły aliasingu), a podgląd bajtów jako unsigned char jest zawsze bezpieczny. Reinterpretacja int jako float to już inna sprawa: wynik zależy całkowicie od reprezentacji na twojej platformie, więc trzymaj to z dala od przenośnego kodu.
Częste błędy
- Odczyt pola, którego nie zapisano. Główne zagrożenie. Używaj etykiety.
- Założenie, że unia konwertuje. Nie konwertuje.
u.i = 3; float f = u.f;reinterpretuje bity;int i = 3; float f = i;konwertuje. Zobacz rzutowanie typów. - Umieszczenie wskaźnika w unii i utrata nad nim kontroli. Jeśli jedna gałąź przechowuje zaalokowany przez ciebie
char *, nadpisanie unii innym polem powoduje wyciek: nic już nie wskazuje na bufor. Zwolnij pamięć, zanim zmienisz gałąź. - Oczekiwanie, że kompilator to sprawdzi. Nie sprawdzi. Unie to jedna z niewielu cech C, przy których język nie oferuje żadnej pomocy poza rozmiarem i wyrównaniem; etykieta to twoja jedyna barierka.
Najczęściej zadawane pytania
Czym jest unia (union) w C?
Unia to typ, którego wszystkie pola współdzielą tę samą pamięć. Zapis jednego pola nadpisuje pozostałe, więc unia przechowuje w danej chwili dokładnie jedno ze swoich pól. Deklaruje się ją jak strukturę, ale ze słowem kluczowym union: union Value { int i; float f; };.
Czym różni się unia od struktury w C?
Struktura daje każdemu polu osobne miejsce w pamięci i przechowuje wszystkie naraz, więc jej rozmiar to co najmniej suma rozmiarów pól. Unia nakłada wszystkie pola pod tym samym adresem i przechowuje jedno naraz, więc jej rozmiar to rozmiar największego pola.
Jaki jest rozmiar unii w C?
Wystarczający dla największego pola, zaokrąglony w górę ze względu na wyrównanie. Unia z int (4 bajty) i double (8 bajtów) zajmuje 8 bajtów, a nie 12. Na własnej platformie sprawdzisz to przez sizeof.
Co się stanie, jeśli odczytasz pole unii, którego nie zapisano?
Zinterpretujesz te same bajty jako inny typ. Zapis u.i = 1, a potem odczyt u.f niczego nie konwertuje: wzorzec bitowy liczby całkowitej zostaje odczytany jako float, co daje bezsensowną liczbę. W standardowym C to w najlepszym razie zachowanie nieokreślone i właśnie dlatego istnieje wzorzec unii z etykietą: obok unii trzymasz etykietę, która mówi, które pole jest aktualne.