Menu

Unia (union) w C: wspólna pamięć, union a struct i unie z etykietą

Jak unia w C przechowuje kilka typów w tych samych bajtach: dlaczego jej rozmiar to rozmiar największego pola, dlaczego trzeba czytać pole zapisane jako ostatnie i jak unia z etykietą (enum plus union) czyni ją bezpieczną.

Na tej stronie są działające edytory: edytuj, uruchamiaj i od razu zobacz wynik.

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 char pozwala 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.

Ilustracja języków programowania w Coddy

Ucz się programowania z Coddy

ZACZNIJ