Odpowiedzi:
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:
typedef union
{
struct {
unsigned char byte1;
unsigned char byte2;
unsigned char byte3;
unsigned char byte4;
} bytes;
unsigned int dword;
} HW_Register;
HW_Register reg;
Następnie możesz uzyskać dostęp do rejestru w następujący sposób:
reg.dword = 0x12345678;
reg.bytes.byte3 = 4;
Endianness (kolejność bajtów) i architektura procesorów są oczywiście ważne.
Kolejną przydatną funkcją jest modyfikator bitów:
typedef union
{
struct {
unsigned char b1:1;
unsigned char b2:1;
unsigned char b3:1;
unsigned char b4:1;
unsigned char reserved:4;
} bits;
unsigned char byte;
} HW_RegisterB;
HW_RegisterB reg;
Za pomocą tego kodu można uzyskać bezpośredni dostęp do jednego bitu w adresie rejestru / pamięci:
x = reg.bits.b2;
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.
typedef union {
unsigned char control_byte;
struct {
unsigned int nibble : 4;
unsigned int nmi : 1;
unsigned int enabled : 1;
unsigned int fired : 1;
unsigned int control : 1;
};
} ControlRegister;
Widziałem to w kilku bibliotekach jako zamiennik dziedziczenia obiektowego.
Na przykład
Connection
/ | \
Network USB VirtualConnection
Jeśli chcesz, aby „klasa” połączenia była jedną z powyższych, możesz napisać coś takiego:
struct Connection
{
int type;
union
{
struct Network network;
struct USB usb;
struct Virtual virtual;
}
};
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
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:
union {
int a;
int b;
int c;
} myUnion;
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.
Wiele zastosowań. Po prostu zrób grep union /usr/include/*
lub w podobnych katalogach. Większość przypadków union
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 kasa man elf
dla rzeczywistych wdrożeń.
To jest podstawowa zasada:
struct _mydata {
int which_one;
union _data {
int a;
float b;
char c;
} foo;
} bar;
switch (bar.which_one)
{
case INTEGER : /* access bar.foo.a;*/ break;
case FLOATING : /* access bar.foo.b;*/ break;
case CHARACTER: /* access bar.foo.c;*/ break;
}
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:
set a to b times 7.
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:
#define ELEM_SYM_SET 0
#define ELEM_SYM_TO 1
#define ELEM_SYM_TIMES 2
#define ELEM_SYM_FULLSTOP 3
#define ELEM_VARIABLE 100
#define ELEM_CONSTANT 101
i do przechowywania każdego elementu użyto następującej struktury:
typedef struct {
int typ;
union {
char *str;
int val;
}
} tElem;
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ć:
tElem e;
e.typ = ELEM_SYM_SET;
Aby utworzyć element „zmienna [b]”, należy użyć:
tElem e;
e.typ = ELEM_VARIABLE;
e.str = strdup ("b"); // make sure you free this later
Aby utworzyć element „stałej [7]”, należy użyć:
tElem e;
e.typ = ELEM_CONSTANT;
e.val = 7;
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
i val
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:
+-----------+
0x1010 | |
0x1011 | typ |
0x1012 | |
0x1013 | |
+-----+-----+
0x1014 | | |
0x1015 | str | val |
0x1016 | | |
0x1017 | | |
+-----+-----+
Gdyby był tylko w strukturze, wyglądałby tak:
+-------+
0x1010 | |
0x1011 | typ |
0x1012 | |
0x1013 | |
+-------+
0x1014 | |
0x1015 | str |
0x1016 | |
0x1017 | |
+-------+
0x1018 | |
0x1019 | val |
0x101A | |
0x101B | |
+-------+
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ę:
struct variant {
int type;
double number;
char *string;
};
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:
struct variant {
int type;
union {
double number;
char *string;
} value;
};
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.
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:
typedef union MyUnion MYUNION;
union MyUnion
{
int MyInt;
float MyFloat;
};
MyUnion będzie zawierał tylko liczbę całkowitą LUB zmiennoprzecinkową, w zależności od tego, który ostatnio ustawiłeś . Robiąc to:
MYUNION u;
u.MyInt = 10;
u ma teraz liczbę całkowitą równą 10;
u.MyFloat = 1.0;
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.
sizeof(int) == sizeof(float)
( == 32
) zwykle.
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.
typedef union
{
UINT8 buffer[PACKET_SIZE]; // Where the packet size is large enough for
// the entire set of fields (including the payload)
struct
{
UINT8 size;
UINT8 cmd;
UINT8 payload[PAYLOAD_SIZE];
UINT8 crc;
} fields;
}PACKET_T;
// This should be called every time a new byte of data is ready
// and point to the packet's buffer:
// packet_builder(packet.buffer, new_data);
void packet_builder(UINT8* buffer, UINT8 data)
{
static UINT8 received_bytes = 0;
// All range checking etc removed for brevity
buffer[received_bytes] = data;
received_bytes++;
// Using the struc only way adds lots of logic that relates "byte 0" to size
// "byte 1" to cmd, etc...
}
void packet_handler(PACKET_T* packet)
{
// Process the fields in a readable manner
if(packet->fields.size > TOO_BIG)
{
// handle error...
}
if(packet->fields.cmd == CMD_X)
{
// do stuff..
}
}
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.
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.
wydarzenie związku { unsigned long eventCode; unsigned char eventParts [4]; };
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”.
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:
union data {
int data;
struct {
unsigned char higher;
unsigned char lower;
} parts;
};
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:
union _Obj {
union _Obj* _M_free_list_link;
char _M_client_data[1]; /* The client sees this. */
};
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.
We wczesnych wersjach C wszystkie deklaracje struktury miały wspólny zestaw pól. Dany:
struct x {int x_mode; int q; float x_f};
struct y {int y_mode; int q; int y_l};
struct z {int z_mode; char name[20];};
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ów struct x
i struct 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 do U*
stanowiło równowartość odlewania go do wskaźnika do dowolnego typu związków zawierających zarówno T
a U
, a następnie rzucając że aby U*
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 z T
na U
. Ponadto, w przypadkach, gdy funkcja otrzymała wskaźnik o nieznanym pochodzeniu, zachowanie zapisu obiektu poprzez T*
konwersjęT*
do a U*
, a następnie odczytanie obiektu przez U*
byłoby równoznaczne z zapisaniem unii za pomocą elementu typu T
i odczytaniem jako typu U
, 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.
foo
ma int
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 zostanie anyPointer
zidentyfikowany każdy typ konstrukcji, który został foo
wymieniony wśród jego członków
anyPointer
identyfikuje się z elementem struktury, to w jaki sposób kompilator sprawdzi te warunki to have a member with the same name only if the type and offset matched
twojego postu?
p->foo
zależy od typu i przesunięcia foo
. Zasadniczo p->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:
union un
{
uint16_t array16[4];
uint32_t array32[2];
}
technicznie jest to nadal naruszenie zasad. ale wszystkie znane standardy obsługują to użycie.