Kiedy należy stosować związki? Dlaczego ich potrzebujemy?
236
Związki są często używane do konwersji między reprezentacjami binarnymi liczb całkowitych i liczb zmiennoprzecinkowych:
union
{
int i;
float f;
} u;
// Convert floating-point bits to integer:
u.f = 3.14159f;
printf("As integer: %08x\n", u.i);
Chociaż jest to technicznie niezdefiniowane zachowanie zgodnie ze standardem C (powinieneś tylko czytać ostatnio napisane pole), będzie działać w dobrze zdefiniowany sposób praktycznie w każdym kompilatorze.
Związki są również czasami używane do implementacji pseudopolimorfizmu w C, poprzez nadanie strukturze jakiegoś znacznika wskazującego, jaki typ obiektu zawiera, a następnie połączenie ze sobą możliwych typów:
enum Type { INTS, FLOATS, DOUBLE };
struct S
{
Type s_type;
union
{
int s_ints[2];
float s_floats[2];
double s_double;
};
};
void do_something(struct S *s)
{
switch(s->s_type)
{
case INTS: // do something with s->s_ints
break;
case FLOATS: // do something with s->s_floats
break;
case DOUBLE: // do something with s->s_double
break;
}
}
Pozwala to na rozmiar struct S
tylko 12 bajtów zamiast 28.
Związki są szczególnie przydatne w programowaniu wbudowanym lub w sytuacjach, w których potrzebny jest bezpośredni dostęp do sprzętu / pamięci. Oto prosty przykład:
Następnie możesz uzyskać dostęp do rejestru w następujący sposób:
Endianness (kolejność bajtów) i architektura procesorów są oczywiście ważne.
Kolejną przydatną funkcją jest modyfikator bitów:
Za pomocą tego kodu można uzyskać bezpośredni dostęp do jednego bitu w adresie rejestru / pamięci:
źródło
Programowanie niskiego poziomu jest rozsądnym przykładem.
IIRC, użyłem związków do rozbicia rejestrów sprzętowych na bity składowe. Możesz więc uzyskać dostęp do 8-bitowego rejestru (jak to było w dniu, w którym to zrobiłem ;-) do bitów składowych.
(Zapomniałem dokładnej składni, ale ...) Ta struktura pozwoliłaby na dostęp do rejestru kontrolnego jako control_byte lub poprzez poszczególne bity. Ważne jest, aby zapewnić odwzorowanie bitów na prawidłowe bity rejestru dla danej endianowości.
źródło
Widziałem to w kilku bibliotekach jako zamiennik dziedziczenia obiektowego.
Na przykład
Jeśli chcesz, aby „klasa” połączenia była jedną z powyższych, możesz napisać coś takiego:
Przykładowe użycie w libinfinity: http://git.0x539.de/?p=infinote.git;a=blob;f=libinfinity/common/inf-session.c;h=3e887f0d63bd754c6b5ec232948027cbbf4d61fc;hb=HEAD#l74
źródło
Związki pozwalają członkom danych, które wykluczają się, na dzielenie tej samej pamięci. Jest to bardzo ważne, gdy pamięci jest mniej, na przykład w systemach wbudowanych.
W poniższym przykładzie:
Ten związek zajmie miejsce pojedynczej liczby int, a nie 3 oddzielnych wartości int. Jeśli użytkownik ustawi wartość a , a następnie wartość b , nadpisze wartość a, ponieważ obaj dzielą tę samą lokalizację pamięci.
źródło
Wiele zastosowań. Po prostu zrób
grep union /usr/include/*
lub w podobnych katalogach. Większość przypadkówunion
jest zawinięte w a,struct
a jeden element struktury mówi, do którego elementu w związku należy się dostać. Na przykład kasaman elf
dla rzeczywistych wdrożeń.To jest podstawowa zasada:
źródło
Oto przykład związku z mojej własnej bazy kodu (z pamięci i sparafrazowany, więc może nie być dokładny). Służyło do przechowywania elementów języka w zbudowanym przeze mnie tłumaczu. Na przykład następujący kod:
składa się z następujących elementów językowych:
Elementy językowe zostały zdefiniowane jako
#define
wartości „ ” w ten sposób:i do przechowywania każdego elementu użyto następującej struktury:
następnie rozmiar każdego elementu był wielkością maksymalnej unii (4 bajty dla typu i 4 bajty dla unii, chociaż są to wartości typowe, rzeczywiste rozmiary zależą od implementacji).
Aby utworzyć element „set”, należy użyć:
Aby utworzyć element „zmienna [b]”, należy użyć:
Aby utworzyć element „stałej [7]”, należy użyć:
i możesz łatwo go rozszerzyć, aby zawierał floats (
float flt
) lub rationals (struct ratnl {int num; int denom;}
) i inne typy.Podstawowym założeniem jest to, że
str
ival
nie są ciągłe w pamięci, faktycznie się nakładają, więc jest to sposób na uzyskanie innego widoku na tym samym bloku pamięci, zilustrowanym tutaj, gdzie struktura jest oparta na miejscu pamięci,0x1010
a liczby całkowite i wskaźniki są zarówno 4 bajty:Gdyby był tylko w strukturze, wyglądałby tak:
źródło
make sure you free this later
komentarz powinien zostać usunięty z elementu stałego?Powiedziałbym, że ułatwia to ponowne wykorzystanie pamięci, którą można wykorzystać na różne sposoby, np. Oszczędzając pamięć. Np. Chciałbyś zrobić strukturę „wariantową”, która może zapisać krótki ciąg, a także liczbę:
W systemie 32-bitowym spowodowałoby to użycie co najmniej 96 bitów lub 12 bajtów dla każdego wystąpienia
variant
.Za pomocą unii możesz zmniejszyć rozmiar do 64 bitów lub 8 bajtów:
Możesz zaoszczędzić jeszcze więcej, jeśli chcesz dodać więcej różnych typów zmiennych itp. Może być prawdą, że możesz robić podobne rzeczy, rzucając pustą wskazówkę - ale unia czyni ją znacznie bardziej dostępną, a także pisz bezpieczny. Takie oszczędności nie wydają się ogromne, ale oszczędzasz jedną trzecią pamięci używanej dla wszystkich instancji tej struktury.
źródło
Trudno wymyślić konkretną okazję, gdy potrzebujesz tego rodzaju elastycznej struktury, być może w protokole wiadomości, w którym wysyłasz wiadomości o różnych rozmiarach, ale nawet wtedy istnieją prawdopodobnie lepsze i bardziej przyjazne dla programistów alternatywy.
Związki są trochę podobne do typów wariantów w innych językach - mogą przechowywać tylko jedną rzecz na raz, ale może to być int, liczba zmiennoprzecinkowa itp., W zależności od tego, jak ją zadeklarujesz.
Na przykład:
MyUnion będzie zawierał tylko liczbę całkowitą LUB zmiennoprzecinkową, w zależności od tego, który ostatnio ustawiłeś . Robiąc to:
u ma teraz liczbę całkowitą równą 10;
u masz teraz liczbę zmienną równą 1,0. Nie ma już int. Oczywiście teraz, jeśli spróbujesz zrobić printf („MyInt =% d”, u.MyInt); wtedy prawdopodobnie pojawi się błąd, chociaż nie jestem pewien, jakie zachowanie będzie miało miejsce.
Rozmiar związku zależy od wielkości jego największego pola, w tym przypadku liczby zmiennoprzecinkowej.
źródło
sizeof(int) == sizeof(float)
(== 32
) zwykle.Związki są używane, gdy chcesz modelować struktury zdefiniowane przez sprzęt, urządzenia lub protokoły sieciowe, lub gdy tworzysz dużą liczbę obiektów i chcesz zaoszczędzić miejsce. Naprawdę nie potrzebujesz ich w 95% przypadków, trzymaj się łatwego do debugowania kodu.
źródło
Wiele z tych odpowiedzi dotyczy rzutowania z jednego typu na inny. Największą korzyść czerpię ze związków tego samego typu, tylko więcej z nich (tj. Podczas analizowania szeregowego strumienia danych). Umożliwiają parsowanie / konstruowanie pakietu w ramce, co staje się banalne.
Edytuj Komentarz na temat endianizmu i wypełniania struktur jest poprawny i ma wielkie obawy. Użyłem tego fragmentu kodu prawie całkowicie we wbudowanym oprogramowaniu, z którego większość kontrolowałem oba końce potoku.
źródło
Związki są świetne. Jednym sprytnym zastosowaniem związków, które widziałem, jest użycie ich podczas definiowania zdarzenia. Na przykład możesz zdecydować, że zdarzenie ma 32 bity.
Teraz, w obrębie tych 32 bitów, możesz chcieć wyznaczyć pierwsze 8 bitów jako identyfikator nadawcy zdarzenia ... Czasami masz do czynienia z wydarzeniem jako całością, czasem analizujesz je i porównujesz jego składniki. związki dają ci elastyczność do robienia obu.
źródło
Co z
VARIANT
tego jest używane w interfejsach COM? Ma dwa pola - „typ” i związek zawierający rzeczywistą wartość, która jest traktowana w zależności od pola „typ”.źródło
W szkole używałem takich związków:
Użyłem go do łatwiejszej obsługi kolorów, zamiast używać operatorów >> i <<, musiałem tylko przejrzeć inny indeks mojej tablicy znaków.
źródło
Użyłem union, kiedy kodowałem dla urządzeń osadzonych. Mam C int, który ma 16 bitów. I muszę odzyskać wyższe 8 bitów i niższe 8 bitów, kiedy muszę czytać z / przechowywać w EEPROM. Użyłem więc w ten sposób:
Nie wymaga zmiany, więc kod jest łatwiejszy do odczytania.
Z drugiej strony widziałem stary kod stl C ++, który używał unii dla alokatora stl. Jeśli jesteś zainteresowany, możesz przeczytać kod źródłowy sgi stl . Oto jego fragment:
źródło
struct
wokółhigher
/lower
? W tej chwili oba powinny wskazywać tylko pierwszy bajt.Spójrz na to: Obsługa poleceń bufora X.25
Jedno z wielu możliwych poleceń X.25 jest odbierane do bufora i obsługiwane w miejscu za pomocą UNION wszystkich możliwych struktur.
źródło
We wczesnych wersjach C wszystkie deklaracje struktury miały wspólny zestaw pól. Dany:
kompilator zasadniczo stworzyłby tabelę rozmiarów struktur (i ewentualnie wyrównania) oraz osobną tabelę nazw, typów i przesunięć elementów konstrukcji. Kompilator nie śledził, które elementy należały do których struktur, i pozwoliłby, aby dwie struktury miały element o tej samej nazwie tylko wtedy, gdy pasowałby typ i przesunięcie (jak w przypadku
q
elementówstruct x
istruct y
). Gdyby p był wskaźnikiem do dowolnego typu struktury, p-> q dodałoby przesunięcie „q” do wskaźnika p i pobierałby „int” z wynikowego adresu.Biorąc pod uwagę powyższą semantykę, możliwe było napisanie funkcji, która mogłaby wykonywać użyteczne operacje na wielu rodzajach konstrukcji zamiennie, pod warunkiem, że wszystkie pola używane przez funkcję były ustawione w jednej linii z użytecznymi polami w danych strukturach. To była przydatna cecha, a zmiana C w celu walidacji elementów używanych do dostępu do struktury względem typów przedmiotowych struktur oznaczałaby utratę jej przy braku środków mających strukturę, która może zawierać wiele nazwanych pól pod tym samym adresem. Dodanie typów „uniowych” do C pomogło nieco wypełnić tę lukę (choć nie IMHO, tak jak powinno być).
Zasadniczą częścią zdolności związków do wypełnienia tej luki był fakt, że wskaźnik do członka związku można przekształcić w wskaźnik do dowolnego związku zawierającego tego członka, a wskaźnik do dowolnego związku można przekształcić w wskaźnik do dowolnego członka. Chociaż standardowa C89 nie wyraźnie powiedzieć, że odlewania
T*
bezpośrednio doU*
stanowiło równowartość odlewania go do wskaźnika do dowolnego typu związków zawierających zarównoT
aU
, a następnie rzucając że abyU*
nie definiuje zachowanie tej ostatniej sekwencji odlewu mogą być naruszone przez użyty typ unii, a Standard nie określił żadnej przeciwnej semantyki dla bezpośredniego rzutowania zT
naU
. Ponadto, w przypadkach, gdy funkcja otrzymała wskaźnik o nieznanym pochodzeniu, zachowanie zapisu obiektu poprzezT*
konwersjęT*
do aU*
, a następnie odczytanie obiektu przezU*
byłoby równoznaczne z zapisaniem unii za pomocą elementu typuT
i odczytaniem jako typuU
, który zostałby zdefiniowany w kilku przypadkach (np. podczas uzyskiwania dostępu do elementów o wspólnej sekwencji początkowej) i zdefiniowany w implementacji (raczej niż Undefined) dla reszty. Chociaż programy rzadko korzystały z gwarancji CIS z rzeczywistymi obiektami typu unii, o wiele bardziej powszechne było wykorzystywanie faktu, że wskaźniki do obiektów niewiadomego pochodzenia musiały zachowywać się jak wskaźniki dla członków związku i związane z nimi gwarancje behawioralne.źródło
foo
maint
przesunięcie 8,anyPointer->foo = 1234;
oznacza to: „weź adres w dowolnym wskaźniku, zastąp go o 8 bajtów i wykonaj zapisywanie liczb całkowitych o wartości 1234 do adresu wynikowego. Kompilator nie będzie musiał wiedzieć ani dbać o to, czy zostanieanyPointer
zidentyfikowany każdy typ konstrukcji, który zostałfoo
wymieniony wśród jego członkówanyPointer
identyfikuje się z elementem struktury, to w jaki sposób kompilator sprawdzi te warunkito have a member with the same name only if the type and offset matched
twojego postu?p->foo
zależy od typu i przesunięciafoo
. Zasadniczop->foo
był skrótem*(typeOfFoo*)((unsigned char*)p + offsetOfFoo)
. Jeśli chodzi o twoje ostatnie pytanie, gdy kompilator napotka definicję członka struktury, wymaga, aby albo nie istniał żaden członek o tej nazwie, albo by członek o tej nazwie miał ten sam typ i przesunięcie; Sądzę, że skurczyłyby się, gdyby istniała niepasująca definicja członka struktury, ale nie wiem, jak radziła sobie z błędami.Prostym i bardzo przydatnym przykładem jest ...
Wyobrażać sobie:
masz
uint32_t array[2]
i chcesz uzyskać dostęp do trzeciego i czwartego bajtu łańcucha bajtów. można zrobić*((uint16_t*) &array[1])
. Ale to niestety łamie surowe zasady aliasingu!Ale znane kompilatory pozwalają wykonać następujące czynności:
technicznie jest to nadal naruszenie zasad. ale wszystkie znane standardy obsługują to użycie.
źródło