Une structure dit « tout cela, ensemble ». Une union dit « exactement une de ces choses, à la fois ». Les membres sont posés les uns sur les autres à la même adresse, donc l'union n'est aussi grande que son plus grand membre et écrire un membre détruit les autres.
Cela ressemble à un piège, et sans contrôle ça l'est. Mais c'est aussi la façon dont le C exprime une valeur pouvant être de plusieurs sortes - un jeton qui est un nombre ou une chaîne, un message qui est un clic ou une frappe de touche - sans payer pour toutes à la fois.
Déclarer et utiliser une union
La syntaxe reflète exactement celle d'une structure ; seul le mot-clé change.
Les membres s'atteignent avec . (ou -> via un pointeur), exactement comme pour une structure. Ce qui diffère, c'est que seul le membre écrit en dernier contient une valeur significative. Après v.f = 3.5f, lire v.i ne vous donne pas 3 - cela vous donne l'entier que le motif de bits de 3.5f se trouve épeler.
La taille : le plus grand membre l'emporte
Comparez la disposition mémoire d'une structure et d'une union aux mêmes membres :
Sur une machine typique, la structure fait 24 octets (4 pour l'int, 8 pour le double, 1 pour le char, plus le remplissage) alors que l'union en fait 8 - la taille de son double, arrondie pour l'alignement. Les trois adresses affichées sont identiques, ce qui résume toute l'histoire des unions en une ligne de sortie.
Notez les casts en void * pour %p. printf attend exactement cela pour %p ; passer un autre type de pointeur est un comportement indéfini même si cela semble généralement fonctionner. Voir spécificateurs de format.
Initialiser une union
Un initialiseur entre accolades sans désignateur initialise le premier membre :
La forme désignée est celle à utiliser. {42} dépend silencieusement de l'ordre des membres, donc réordonner la déclaration plus tard change quel membre est initialisé - un bug vraiment vicieux, car rien dans le code ne semble différent.
Le vrai problème : quel membre est vivant ?
Une union n'enregistre pas quel membre vous avez écrit en dernier. Ce ne sont que des octets ; la connaissance vit dans votre tête, et c'est exactement là que la connaissance se perd.
Le nombre affiché est grand et étrange - le motif de bits de 1.0f lu comme un int. Rien n'a planté, rien n'a averti, et le programme est discrètement faux. L'union a fait exactement ce qu'elle promettait ; l'erreur était la nôtre, en oubliant quel membre était vivant.
La solution : une union étiquetée
La solution standard est d'apparier l'union à une enum qui enregistre le membre vivant, et d'envelopper les deux dans une structure. Cette combinaison s'appelle une union étiquetée (ou discriminée), et c'est ainsi que vous devriez écrire pratiquement toutes les unions dans du code applicatif.
Chaque lecture passe désormais par le switch sur kind, il est donc impossible de lire un membre jamais écrit - tant que chaque écriture pose aussi l'étiquette. Envelopper les écritures dans de petites fonctions constructrices (value_from_int, value_from_string) est la façon habituelle de rendre l'oubli impossible.
L'économie de mémoire est réelle : chaque Value coûte ici 24 octets de charge utile plus l'étiquette, au lieu de 4 + 4 + 24 pour une structure contenant les trois. Avec cent mille d'entre elles, cela compte.
Compiler avec -Wall ajoute un second filet : si vous ajoutez plus tard un VAL_BOOL à l'enum et oubliez un case pour lui, GCC avertit sur la valeur d'énumération non traitée.
Les unions anonymes
C11 autorise un membre union sans nom dans une structure, promouvant ses membres dans l'espace de noms de la structure englobante :
Parce que l'union elle-même est sans nom, vous écrivez s->circle.r plutôt que s->as.circle.r. Plus court à lire, au prix de masquer le fait même qu'une union est en jeu - ce qui convient quand l'étiquette est juste à côté.
Là où les unions justifient vraiment leur place
Quatre usages récurrents :
- Les valeurs variantes. Les interpréteurs, les analyseurs JSON et de configuration, et les files de messages portent tous des valeurs dont le type est décidé à l'exécution. Une union étiquetée en est la représentation canonique.
- Les enregistrements serrés en mémoire. Quand une structure a plusieurs champs mutuellement exclusifs et que vous en avez des millions, les superposer est une économie directe.
- Les dispositions de protocoles et de matériel. Un paquet dont la charge utile dépend d'un octet d'en-tête se projette naturellement sur une union étiquetée, tout comme les registres d'un périphérique.
- L'inspection d'octets. Superposer une valeur avec un tableau
unsigned char[]permet de regarder ses octets individuels, par exemple pour déterminer le boutisme :
C'est du type punning - relire délibérément des octets comme un autre type. Lire à travers une union ainsi est explicitement autorisé avec un résultat défini par l'implémentation en C (contrairement au cast de pointeurs entre types sans rapport, qui casse les règles d'aliasing), et inspecter des octets en unsigned char est toujours sûr. Réinterpréter un int comme un float est autre chose : le résultat dépend entièrement de la représentation de votre plateforme, gardez-le donc hors du code portable.
Erreurs courantes
- Lire un membre que vous n'avez pas écrit. Le danger phare. Utilisez une étiquette.
- Supposer qu'une union convertit. Elle ne convertit pas.
u.i = 3; float f = u.f;réinterprète des bits ;int i = 3; float f = i;convertit. Voir transtypage. - Mettre un pointeur dans une union et le perdre de vue. Si une branche contient un
char *que vous avez alloué, écraser l'union avec un autre membre le fait fuir - plus rien ne pointe sur le tampon. Libérez avant de changer de branche. - Attendre du compilateur qu'il vérifie. Il ne le fera pas. Les unions sont l'une des rares fonctionnalités C où le langage n'offre aucune aide au-delà de la taille et de l'alignement ; l'étiquette est votre seul garde-fou.
Questions fréquentes
Qu'est-ce qu'une union en C ?
Une union est un type dont tous les membres partagent la même mémoire. Écrire un membre écrase les autres, donc une union ne contient exactement qu'un seul de ses membres à la fois. Elle se déclare comme une structure mais avec le mot-clé union : union Value { int i; float f; };.
Quelle est la différence entre une union et une structure en C ?
Une structure donne à chaque membre son propre stockage et les contient tous à la fois, donc sa taille vaut au moins la somme de ses membres. Une union superpose chaque membre à la même adresse et n'en contient qu'un à la fois, donc sa taille est celle de son plus grand membre.
Quelle est la taille d'une union en C ?
Assez grande pour son plus grand membre, arrondie pour l'alignement. Une union d'un int (4 octets) et d'un double (8 octets) fait 8 octets - pas 12. sizeof est la façon de vérifier sur votre propre plateforme.
Que se passe-t-il si on lit un membre d'union qu'on n'a pas écrit ?
Vous réinterprétez les mêmes octets comme un autre type. Écrire u.i = 1 puis lire u.f ne convertit pas - cela lit le motif de bits de l'entier comme un flottant, donnant un nombre sans signification. En C standard, c'est au mieux non spécifié, c'est pourquoi le motif d'union étiquetée existe : gardez à côté une étiquette disant quel membre est vivant.