Réponses:
Les unions sont souvent utilisées pour convertir entre les représentations binaires d'entiers et de flottants:
union
{
int i;
float f;
} u;
// Convert floating-point bits to integer:
u.f = 3.14159f;
printf("As integer: %08x\n", u.i);
Bien qu'il s'agisse d'un comportement techniquement indéfini selon la norme C (vous n'êtes censé lire que le champ le plus récemment écrit), il agira de manière bien définie dans pratiquement tous les compilateurs.
Les unions sont également parfois utilisées pour implémenter un pseudo-polymorphisme en C, en donnant à une structure une balise indiquant le type d'objet qu'elle contient, puis en réunissant les types possibles ensemble:
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;
}
}
Cela permet à la taille de struct S
n'être que de 12 octets, au lieu de 28.
Les unions sont particulièrement utiles dans la programmation intégrée ou dans les situations où un accès direct au matériel / à la mémoire est nécessaire. Voici un exemple trivial:
typedef union
{
struct {
unsigned char byte1;
unsigned char byte2;
unsigned char byte3;
unsigned char byte4;
} bytes;
unsigned int dword;
} HW_Register;
HW_Register reg;
Ensuite, vous pouvez accéder au reg comme suit:
reg.dword = 0x12345678;
reg.bytes.byte3 = 4;
L'endianisme (ordre des octets) et l'architecture du processeur sont bien sûr importants.
Une autre fonctionnalité utile est le modificateur de bits:
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;
Avec ce code, vous pouvez accéder directement à un seul bit dans l'adresse de registre / mémoire:
x = reg.bits.b2;
La programmation système de bas niveau est un exemple raisonnable.
IIRC, j'ai utilisé des unions pour décomposer les registres matériels en bits de composants. Ainsi, vous pouvez accéder à un registre 8 bits (tel qu'il était, le jour où je l'ai fait ;-) dans les bits des composants.
(J'oublie la syntaxe exacte mais ...) Cette structure permettrait d'accéder à un registre de contrôle en tant que control_byte ou via les bits individuels. Il serait important de s'assurer que les bits correspondent aux bits de registre corrects pour une endianité donnée.
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;
Je l'ai vu dans quelques bibliothèques en remplacement de l'héritage orienté objet.
Par exemple
Connection
/ | \
Network USB VirtualConnection
Si vous voulez que la "classe" Connection soit l'une des deux ci-dessus, vous pouvez écrire quelque chose comme:
struct Connection
{
int type;
union
{
struct Network network;
struct USB usb;
struct Virtual virtual;
}
};
Exemple d'utilisation dans libinfinity: http://git.0x539.de/?p=infinote.git;a=blob;f=libinfinity/common/inf-session.c;h=3e887f0d63bd754c6b5ec232948027cbbf4d61fc;hb=HEAD#l74
Les unions permettent aux membres de données qui s'excluent mutuellement de partager la même mémoire. Ceci est très important lorsque la mémoire est plus rare, comme dans les systèmes embarqués.
Dans l'exemple suivant:
union {
int a;
int b;
int c;
} myUnion;
Cette union prendra l'espace d'un seul int, plutôt que de 3 valeurs int distinctes. Si l'utilisateur définissait la valeur de a , puis définissait la valeur de b , il écraserait la valeur de a car ils partagent tous les deux le même emplacement de mémoire.
Beaucoup d'utilisations. Faites-le grep union /usr/include/*
ou dans des répertoires similaires. La plupart des cas, le union
est enveloppé dans un struct
et un membre de la structure indique à quel élément de l'union accéder. Par exemple, vérifier man elf
les implémentations réelles.
C'est le principe de base:
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;
}
Voici un exemple d'une union de ma propre base de code (de mémoire et paraphrasée donc elle peut ne pas être exacte). Il a été utilisé pour stocker des éléments de langage dans un interpréteur que j'ai construit. Par exemple, le code suivant:
set a to b times 7.
se compose des éléments de langage suivants:
Les éléments de langage ont été définis comme #define
des valeurs ' ' ainsi:
#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
et la structure suivante a été utilisée pour stocker chaque élément:
typedef struct {
int typ;
union {
char *str;
int val;
}
} tElem;
alors la taille de chaque élément était la taille de l'union maximale (4 octets pour le type et 4 octets pour l'union, bien que ce soient des valeurs typiques, les tailles réelles dépendent de l'implémentation).
Afin de créer un élément "set", vous utiliseriez:
tElem e;
e.typ = ELEM_SYM_SET;
Afin de créer un élément "variable [b]", vous utiliseriez:
tElem e;
e.typ = ELEM_VARIABLE;
e.str = strdup ("b"); // make sure you free this later
Afin de créer un élément "constant [7]", vous utiliseriez:
tElem e;
e.typ = ELEM_CONSTANT;
e.val = 7;
et vous pouvez facilement l'étendre pour inclure float ( float flt
) ou rationals ( struct ratnl {int num; int denom;}
) et d'autres types.
La prémisse de base est que les str
et val
ne sont pas contigus en mémoire, ils se chevauchent, c'est donc un moyen d'obtenir une vue différente sur le même bloc de mémoire, illustré ici, où la structure est basée sur l'emplacement de la mémoire 0x1010
et les entiers et les pointeurs sont tous les deux 4 octets:
+-----------+
0x1010 | |
0x1011 | typ |
0x1012 | |
0x1013 | |
+-----+-----+
0x1014 | | |
0x1015 | str | val |
0x1016 | | |
0x1017 | | |
+-----+-----+
Si c'était juste dans une structure, cela ressemblerait à ceci:
+-------+
0x1010 | |
0x1011 | typ |
0x1012 | |
0x1013 | |
+-------+
0x1014 | |
0x1015 | str |
0x1016 | |
0x1017 | |
+-------+
0x1018 | |
0x1019 | val |
0x101A | |
0x101B | |
+-------+
make sure you free this later
commentaire doit-il être supprimé de l'élément constant?
Je dirais que cela facilite la réutilisation de la mémoire qui pourrait être utilisée de différentes manières, c'est-à-dire économiser de la mémoire. Par exemple, vous souhaitez faire une structure "variante" capable d'enregistrer une chaîne courte ainsi qu'un nombre:
struct variant {
int type;
double number;
char *string;
};
Dans un système 32 bits, cela entraînerait l'utilisation d'au moins 96 bits ou 12 octets pour chaque instance de variant
.
En utilisant une union, vous pouvez réduire la taille à 64 bits ou 8 octets:
struct variant {
int type;
union {
double number;
char *string;
} value;
};
Vous pouvez économiser encore plus si vous souhaitez ajouter plus de types de variables différents, etc. Il est possible que vous puissiez faire des choses similaires en créant un pointeur vide - mais l'union le rend beaucoup plus accessible ainsi que le type sûr. Ces économies ne semblent pas énormes, mais vous économisez un tiers de la mémoire utilisée pour toutes les instances de cette structure.
Il est difficile de penser à une occasion spécifique où vous auriez besoin de ce type de structure flexible, peut-être dans un protocole de message où vous enverriez différentes tailles de messages, mais même dans ce cas, il existe probablement des alternatives meilleures et plus conviviales pour les programmeurs.
Les unions sont un peu comme des types de variantes dans d'autres langues - elles ne peuvent contenir qu'une seule chose à la fois, mais cette chose peut être un entier, un flottant, etc., selon la façon dont vous la déclarez.
Par exemple:
typedef union MyUnion MYUNION;
union MyUnion
{
int MyInt;
float MyFloat;
};
MyUnion ne contiendra qu'un int OU un flottant, selon celui que vous avez défini le plus récemment . Ce faisant:
MYUNION u;
u.MyInt = 10;
u détient maintenant un int égal à 10;
u.MyFloat = 1.0;
u détient désormais un flottant égal à 1,0. Il ne contient plus d'int. Évidemment maintenant si vous essayez de faire printf ("MyInt =% d", u.MyInt); vous obtiendrez probablement une erreur, même si je ne suis pas sûr du comportement spécifique.
La taille de l'union est dictée par la taille de son plus grand champ, dans ce cas le flotteur.
sizeof(int) == sizeof(float)
( == 32
) généralement.
Les unions sont utilisées lorsque vous souhaitez modéliser des structures définies par du matériel, des périphériques ou des protocoles réseau, ou lorsque vous créez un grand nombre d'objets et souhaitez économiser de l'espace. Vous n'avez vraiment pas besoin d'eux 95% du temps, restez avec du code facile à déboguer.
Beaucoup de ces réponses concernent la conversion d'un type à un autre. J'obtiens le plus d'utilisation des unions avec les mêmes types juste un peu plus (c'est-à-dire lors de l'analyse d'un flux de données série). Ils permettent à l'analyse / construction d'un paquet encadré de devenir trivial.
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..
}
}
Modifier Les commentaires sur l'endianité et le remplissage de struct sont des préoccupations valables et importantes. J'ai utilisé ce corps de code presque entièrement dans des logiciels embarqués, dont la plupart j'avais le contrôle des deux extrémités du tuyau.
Les syndicats sont super. Une utilisation intelligente des syndicats que j'ai vu consiste à les utiliser lors de la définition d'un événement. Par exemple, vous pouvez décider qu'un événement est de 32 bits.
Maintenant, à l'intérieur de ces 32 bits, vous aimeriez peut-être désigner les 8 premiers bits comme identifiant de l'expéditeur de l'événement ... Parfois, vous traitez l'événement dans son ensemble, parfois vous le disséquez et comparez ses composants. les syndicats vous donnent la flexibilité de faire les deux.
événement syndical { unsigned long eventCode; unsigned char eventParts [4]; };
À l'école, j'ai utilisé des syndicats comme celui-ci:
typedef union
{
unsigned char color[4];
int new_color;
} u_color;
Je l'ai utilisé pour gérer les couleurs plus facilement, au lieu d'utiliser les opérateurs >> et <<, je n'ai eu qu'à parcourir les différents index de mon tableau de caractères.
J'ai utilisé l'union lorsque je codais pour des appareils intégrés. J'ai C int qui est de 16 bits de long. Et je dois récupérer les 8 bits supérieurs et les 8 bits inférieurs lorsque je dois lire / stocker dans l'EEPROM. J'ai donc utilisé cette méthode:
union data {
int data;
struct {
unsigned char higher;
unsigned char lower;
} parts;
};
Il ne nécessite pas de décalage, le code est donc plus facile à lire.
D'un autre côté, j'ai vu un ancien code stl C ++ qui utilisait l'union pour l'allocateur stl. Si vous êtes intéressé, vous pouvez lire le code source sgi stl . En voici un extrait:
union _Obj {
union _Obj* _M_free_list_link;
char _M_client_data[1]; /* The client sees this. */
};
struct
autour de votre higher
/ lower
? À l'heure actuelle, les deux doivent pointer uniquement vers le premier octet.
Regarde ça: gestion des commandes de tampon X.25
L'une des nombreuses commandes X.25 possibles est reçue dans un tampon et gérée en place à l'aide d'un UNION de toutes les structures possibles.
Dans les premières versions de C, toutes les déclarations de structure partageraient un ensemble commun de champs. Donné:
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];};
un compilateur produirait essentiellement un tableau des tailles des structures (et éventuellement des alignements), et un tableau séparé des noms, types et décalages des membres des structures. Le compilateur ne garde pas la trace des membres appartenant à quelles structures et autorise deux structures à avoir un membre du même nom uniquement si le type et le décalage correspondent (comme pour les membres q
de struct x
etstruct y
). Si p était un pointeur vers n'importe quel type de structure, p-> q ajouterait le décalage de "q" au pointeur p et récupérerait un "int" à partir de l'adresse résultante.
Compte tenu de la sémantique ci-dessus, il était possible d'écrire une fonction qui pourrait effectuer des opérations utiles sur plusieurs types de structure de manière interchangeable, à condition que tous les champs utilisés par la fonction soient alignés avec des champs utiles dans les structures en question. C'était une fonctionnalité utile, et changer C pour valider les membres utilisés pour l'accès à la structure par rapport aux types de structures en question aurait signifié la perdre en l'absence d'un moyen d'avoir une structure pouvant contenir plusieurs champs nommés à la même adresse. L'ajout de types «union» à C a aidé à combler quelque peu cette lacune (mais pas à mon humble avis, comme il aurait dû l'être).
Un élément essentiel de la capacité des syndicats à combler cette lacune était le fait qu'un pointeur vers un membre d'union pouvait être converti en pointeur vers n'importe quel syndicat contenant ce membre, et qu'un pointeur vers n'importe quel syndicat pouvait être converti en pointeur vers n'importe quel membre. Bien que la norme C89 ne dise pas expressément que la conversion T*
directe d'un a U*
était équivalente à la conversion en un pointeur vers tout type d'union contenant les deux T
et U
, puis à la conversion U*
, aucun comportement défini de la dernière séquence de conversion ne serait affecté par le type d'union utilisé, et la norme n'a pas spécifié de sémantique contraire pour une conversion directe de T
à U
. De plus, dans les cas où une fonction a reçu un pointeur d'origine inconnue, le comportement d'écriture d'un objet via T*
, la conversion duT*
unU*
, puis lire l'objet via U*
équivaudrait à écrire une union via membre de type T
et à lire comme type U
, qui serait défini dans certains cas (par exemple lors de l'accès aux membres de la séquence initiale commune) et défini par l'implémentation (plutôt qu'indéfini) ) pour le reste. S'il était rare que des programmes exploitent les garanties de la CEI avec des objets réels de type syndical, il était beaucoup plus courant d'exploiter le fait que les pointeurs vers des objets d'origine inconnue devaient se comporter comme des pointeurs vers des membres du syndicat et avoir les garanties comportementales qui y étaient associées.
foo
est un int
avec un décalage 8, cela anyPointer->foo = 1234;
signifie "prendre l'adresse dans anyPointer, la déplacer de 8 octets et effectuer un stockage entier de la valeur 1234 à l'adresse résultante. Le compilateur n'aurait pas besoin de savoir ou de se soucier s'il est anyPointer
identifié tout type de structure ayant foo
anyPointer
identifie avec un membre struct, alors comment le compilateur vérifie-t-il ces conditions to have a member with the same name only if the type and offset matched
de votre publication?
p->foo
dépendrait du type et du décalage de foo
. Essentiellement, p->foo
c'était un raccourci pour *(typeOfFoo*)((unsigned char*)p + offsetOfFoo)
. Quant à votre dernière question, lorsqu'un compilateur rencontre une définition de membre struct, il requiert qu'aucun membre portant ce nom n'existe, ou que le membre portant ce nom ait le même type et le même décalage; Je suppose que le aurait crié si une définition de membre struct non correspondant existait, mais je ne sais pas comment il a géré les erreurs.
Un exemple simple et très utile est ...
Imaginer:
vous avez un uint32_t array[2]
et souhaitez accéder aux 3e et 4e octets de la chaîne Byte. tu pourrais faire *((uint16_t*) &array[1])
. Mais cela enfreint malheureusement les règles strictes d'alias!
Mais les compilateurs connus vous permettent de faire ce qui suit:
union un
{
uint16_t array16[4];
uint32_t array32[2];
}
techniquement, il s'agit toujours d'une violation des règles. mais toutes les normes connues prennent en charge cette utilisation.