Pourquoi avons-nous besoin de C Unions?

Quand faut-il utiliser les syndicats? Pourquoi avons-nous besoin d’eux?

Les unions sont souvent utilisées pour convertir entre les représentations binarys d’entiers et de flottants:

union { int i; float f; } u; // Convert floating-point bits to integer: uf = 3.14159f; printf("As integer: %08x\n", ui); 

Bien qu’il s’agisse d’un comportement techniquement indéfini selon le standard C (vous êtes seulement censé lire le champ le plus récemment écrit), il agira de manière bien définie dans pratiquement tous les compilateurs.

Les unions sont aussi parfois utilisées pour implémenter un pseudo-polymorphism en C, en donnant une structure à une étiquette indiquant le type d’object qu’elle contient, puis en regroupant les types possibles:

 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 que la taille de struct S ne soit 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 access direct au matériel / à la mémoire est nécessaire. Voici un exemple sortingvial:

 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 registre comme suit:

 reg.dword = 0x12345678; reg.bytes.byte3 = 4; 

L’endianness (ordre des octets) et l’architecture du processeur sont évidemment importants.

Une autre fonctionnalité utile est le modificateur de bit:

 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’enregistrement du registre / de la mémoire:

 x = reg.bits.b2; 

La programmation système de bas niveau en est un exemple raisonnable.

L’IIRC, j’ai utilisé des syndicats pour décomposer les registres de matériel en bits de composants. Donc, vous pouvez accéder à un registre de 8 bits (tel quel, le jour où je l’ai fait 😉 dans les bits du composant.

(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’inheritance orienté object.

Par exemple

  Connection / | \ Network USB VirtualConnection 

Si vous voulez que la classe “Connection” soit l’une des précédentes, 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 mutuellement exclusifs 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 occupera l’espace d’un seul int, plutôt que 3 valeurs int distinctes. Si l’utilisateur définit la valeur de a , puis définit la valeur de b , il écrase la valeur de a car ils partagent tous les deux le même emplacement de mémoire.

Beaucoup d’utilisations. Faites simplement grep union /usr/include/* ou dans des répertoires similaires. La plupart des cas où l’ union est enveloppée dans une struct et un membre de la structure indique quel élément de l’union doit y accéder. Par exemple, un man elf caisse pour des 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’union de ma propre base de code (de mémoire et paraphrasée pour 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 linguistiques suivants:

  • symbole [set]
  • variable [a]
  • symbole [à]
  • variable [b]
  • symbole [fois]
  • constante [7]
  • symbole[.]

Les éléments de langage ont été définis comme des valeurs ” #define “:

 #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 cepend sur l’implémentation).

Pour créer un élément “set”, vous utiliseriez:

 tElem e; e.typ = ELEM_SYM_SET; 

Pour 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 

Pour créer un élément “constant [7]”, vous devez utiliser:

 tElem e; e.typ = ELEM_CONSTANT; e.val = 7; 

et vous pourriez facilement l’étendre pour inclure des flottants ( float flt ) ou des rationnels ( struct ratnl {int num; int denom;} ) et d’autres types.

Le principe de base est que les str et val ne sont pas contigus dans la mémoire, ils se chevauchent en réalité, c’est donc un moyen d’obtenir une vue différente sur le même bloc de mémoire, illustré ici par l’emplacement mémoire 0x1010 et les entiers et les pointeurs sont tous 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 | | +-------+ 

Je dirais que cela facilite la réutilisation de la mémoire qui pourrait être utilisée de différentes manières, par exemple en économisant de la mémoire. Par exemple, vous souhaitez créer une structure “variante” capable de sauvegarder une chaîne courte ainsi qu’un numéro:

 struct variant { int type; double number; char *ssortingng; }; 

Dans un système 32 bits, au moins 96 bits ou 12 octets seraient utilisés 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 *ssortingng; } value; }; 

Vous pouvez économiser encore plus si vous souhaitez append des types de variables différents, etc. Il est peut-être vrai que vous pouvez faire des choses similaires en jetant 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 messagerie où vous enverriez différentes tailles de messages, mais même dans ce cas, il existe des alternatives plus conviviales et plus conviviales.

Les unions sont un peu comme les types de variantes dans d’autres langages – elles ne peuvent contenir qu’une chose à la fois, mais cela peut être un int, un float, etc. selon la manière dont vous le déclarez.

Par exemple:

 typedef union MyUnion MYUNION; union MyUnion { int MyInt; float MyFloat; }; 

MyUnion ne contiendra qu’un int ou un flottant, selon ce que vous avez défini le plus récemment . Donc en faisant ceci:

 MYUNION u; u.MyInt = 10; 

Vous avez maintenant un int égal à 10;

 u.MyFloat = 1.0; 

Vous avez maintenant un flottant égal à 1.0. Il ne tient plus un int. Évidemment maintenant si vous essayez de faire printf (“MyInt =% d”, u.MyInt); alors vous allez probablement recevoir une erreur, même si je ne suis pas certain du comportement spécifique.

La taille de l’union est dictée par la taille de son plus grand champ, en l’occurrence le flottant.

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’objects et souhaitez économiser de l’espace. Vous n’en avez pas vraiment besoin 95% du temps, respectez le code facile à déboguer.

Beaucoup de ces réponses traitent du casting d’un type à un autre. J’utilise au maximum les unions avec les mêmes types (par exemple, lors de l’parsing d’un stream de données série). Ils permettent à l’parsing / construction d’un paquet encadré de devenir sortingvial.

 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 Le commentaire sur l’endianness et le remplissage de structure est une préoccupation importante. J’ai utilisé ce corpus de code presque entièrement dans des logiciels embarqués, dont la plupart contrôlaient les deux extrémités du tube.

Les syndicats sont formidables. Une utilisation judicieuse des unions que j’ai vues est de 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 voudrez 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 vous le comparez. les syndicats vous donnent la flexibilité de faire les deux.

 événement syndical
 {
   unsign long eventCode;
   unsigned char eventParts [4];
 };

Qu’en est-il de VARIANT utilisé dans les interfaces COM? Il comporte deux champs – “type” et une union contenant une valeur réelle traitée en fonction du champ “type”.

À 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 devais simplement parcourir l'index différent de mon tableau de caractères.

J’ai utilisé l’union quand je codais pour les appareils embarqués. J’ai C int qui est long de 16 bits. Et j’ai besoin de récupérer les 8 bits supérieurs et les 8 bits inférieurs lorsque je dois lire depuis / stocker vers l’EEPROM. J’ai donc utilisé cette façon:

 union data { int data; struct { unsigned char higher; unsigned char lower; } parts; }; 

Il n’est pas nécessaire de changer de nom pour que le code soit plus facile à lire.

D’autre part, j’ai vu un ancien code stl de C ++ qui utilisait l’union pour stl allocator. Si vous êtes intéressé, vous pouvez lire le code source de sgi stl . En voici une partie:

 union _Obj { union _Obj* _M_free_list_link; char _M_client_data[1]; /* The client sees this. */ }; 
  • Un fichier contenant différents types d’enregistrement.
  • Une interface réseau contenant différents types de requêtes.

Jetez un coup d’oeil à ceci: Gestion de la commande tampon X.25

L’une des nombreuses commandes X.25 possibles est reçue dans un tampon et gérée en place en utilisant une 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 une table des tailles des structures (et éventuellement des alignements) et une table séparée des noms, types et décalages des membres des structures. Le compilateur ne gardait pas la trace des membres appartenant à quelles structures, et permettait à deux structures d’avoir un membre du même nom uniquement si le type et le décalage correspondaient (comme avec le membre q de struct x et struct y ). Si p était un pointeur sur un type de structure, p-> q appendait le décalage de “q” au pointeur p et irait chercher un “int” à l’adresse résultante.

Compte tenu de la sémantique ci-dessus, il était possible d’écrire de façon interchangeable une fonction pouvant effectuer des opérations utiles sur plusieurs types de structure, à 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 fonction utile, et changer C pour valider les membres utilisés pour l’access à la structure par rapport aux types de structures en question aurait signifié le 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 permis de combler un peu cet écart (mais pas à mon humble avis, aussi bien que cela aurait dû être).

Un élément essentiel de la capacité des syndicats à combler cette lacune était le fait qu’un pointeur sur un membre d’union pouvait être converti en un pointeur vers une union contenant ce membre et qu’un pointeur sur une union quelconque pouvait être converti en un pointeur sur un membre. Alors que le standard C89 ne disait pas expressément que lancer un T* directement sur un U* équivaut à le convertir en un pointeur vers un type d’union contenant à la fois T et U , puis en U* , aucun comportement défini du cette dernière séquence serait affectée par le type d’union utilisé, et la norme ne spécifiait aucune sémantique contraire pour une dissortingbution directe de T vers U De plus, dans les cas où une fonction recevrait un pointeur d’origine inconnue, le comportement d’écriture d’un object via T* , en convertissant le T* en U* et en lisant l’object via U* serait équivalent à écrire une union via un membre de type T et en lisant le type U , qui serait défini par la norme dans quelques cas (par exemple, lors de l’access aux membres de la séquence initiale commune) et défini par l’implémentation (plutôt que non défini) pour le rest. S’il était rare que des programmes exploitent les garanties du CIS avec des objects de type union, il était beaucoup plus courant d’exploiter le fait que les pointeurs vers des objects d’origine inconnue devaient se comporter comme des pointeurs

Un exemple simple et très utile, c’est ….

Imaginer:

vous avez un uint32_t array[2] et souhaitez accéder aux 3ème et 4ème octets de la chaîne Byte. vous pourriez faire *((uint16_t*) &array[1]) . Mais cela enfreint sortingstement les règles ssortingctes d’aliasing!

Mais les compilateurs connus vous permettent d’effectuer les opérations suivantes:

 union un { uint16_t array16[4]; uint32_t array32[2]; } 

techniquement, cela rest une violation des règles. mais toutes les normes connues prennent en charge cette utilisation.