Pourquoi avons-nous besoin de C Unions?


236

Quand utiliser les syndicats? Pourquoi avons-nous besoin d'eux?

Réponses:


252

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 Sn'être que de 12 octets, au lieu de 28.


il devrait y avoir uy au lieu d'uf
Amit Singh Tomar

1
Est-ce que l'exemple qui suppose de convertir float en entier fonctionne? Je ne pense pas, car int et float sont stockés dans différents formats en mémoire. Pouvez-vous expliquer votre exemple?
spin_eight

3
@spin_eight: Il ne s'agit pas de "convertir" de float en int. Plus comme "réinterpréter la représentation binaire d'un flottant comme s'il s'agissait d'un int". La sortie n'est pas 3: ideone.com/MKjwon Je ne sais pas pourquoi Adam imprime en hexadécimal, cependant.
endolith

@Adam Rosenfield je n'ai pas vraiment compris la conversion je n'ai pas d'entier dans la sortie: p
The Beast

2
Je pense que la clause de non-responsabilité concernant les comportements non définis devrait être supprimée. C'est, en fait, un comportement défini. Voir la note de bas de page 82 de la norme C99: si le membre utilisé pour accéder au contenu d'un objet union n'est pas le même que le dernier membre utilisé pour stocker une valeur dans l'objet, la partie appropriée de la représentation de la valeur de l'objet est réinterprétée comme une représentation d'objet dans le nouveau type comme décrit en 6.2.6 (un processus parfois appelé "type punning"). Cela pourrait être une représentation piège.
Christian Gibbons

136

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;

3
Votre réponse ici conjointement avec la réponse de @Adam Rosenfield ci-dessus fait la paire complémentaire parfaite: vous démontrez l'utilisation d'une structure au sein d'une union , et il démontre l'utilisation d'une union au sein d'une structure . Il s'avère que j'ai besoin des deux à la fois: une structure au sein d'une union dans une structure pour implémenter un polymorphisme de passage de message sophistiqué en C entre les threads sur un système intégré, et je ne me serais pas rendu compte que si je n'avais pas vu vos deux réponses ensemble .
Gabriel Staples

1
J'avais tort: ​​c'est une union dans une structure dans une union dans une structure, imbriquée à gauche pour écrire comme je l'ai écrit, du niveau le plus imbriqué au plus externe. J'ai dû ajouter une autre union au niveau le plus interne pour autoriser les valeurs de différents types de données.
Gabriel Staples

64

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;

3
Ceci est un excellent exemple! Voici un exemple de la façon dont vous pouvez utiliser cette technique dans les logiciels intégrés: edn.com/design/integrated-circuit-design/4394915/…
rzetterberg

34

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


33

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.


29

Beaucoup d'utilisations. Faites-le grep union /usr/include/*ou dans des répertoires similaires. La plupart des cas, le unionest enveloppé dans un structet un membre de la structure indique à quel élément de l'union accéder. Par exemple, vérifier man elfles 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;
}

Exactement ce que je cherchais! Très utile pour remplacer certains paramètres de points de suspension :)
Nicolas Voron

17

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:

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

Les éléments de langage ont été définis comme #definedes 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 stret valne 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 0x1010et 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 |       |
       +-------+

Le make sure you free this latercommentaire doit-il être supprimé de l'élément constant?
Trevor

Oui, @Trevor, bien que je ne puisse pas croire que vous soyez la première personne à l'avoir vu au cours des 4+ dernières années :-) Corrigé, et merci pour cela.
paxdiablo

7

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.


5

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.


1
sizeof(int) == sizeof(float)( == 32) généralement.
Nick T

1
Pour l'enregistrement, l'attribution au flottant puis l'impression de l'int ne provoquera pas d'erreur, car ni le compilateur ni l'environnement d'exécution ne savent quelle valeur est valide. L'int int qui sera imprimé n'aura bien sûr aucun sens pour la plupart des utilisations. Ce sera juste la représentation mémoire du flottant, interprétée comme un int.
Jerry B

4

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.


4

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.


1
Ce code ne fonctionnera pas (la plupart du temps) si des données sont échangées sur 2 plates-formes différentes pour les raisons suivantes: 1) L'endianité peut être différente. 2) Rembourrage dans les structures.
Mahori

@Ravi Je suis d'accord avec les préoccupations concernant l'endianisme et le rembourrage. Cependant, il faut savoir que je l'ai utilisé exclusivement dans des projets intégrés. La plupart desquels je contrôlais les deux extrémités des tuyaux.
Adam Lewis

1

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];
};

1

Qu'en est-il VARIANTqui est utilisé dans les interfaces COM? Il a deux champs - "type" et une union contenant une valeur réelle qui est traitée en fonction du champ "type".


1

À 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.


1

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.        */
};

1
N'auriez-vous pas besoin d'un regroupement structautour de votre higher/ lower? À l'heure actuelle, les deux doivent pointer uniquement vers le premier octet.
Mario

@Mario ah oui, je l'écris juste à la main et je l'oublie, merci
Mu Qiao

1
  • Un fichier contenant différents types d'enregistrement.
  • Une interface réseau contenant différents types de requêtes.

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.


pourriez-vous s'il vous plaît expliquer ces deux exemples. Je veux dire comment ils sont liés à l'union
Amit Singh Tomar

1

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 qde struct xetstruct 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 Tet 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 Tet à 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.


pouvez-vous donner un exemple de ceci: «il était possible d'écrire une fonction qui pourrait effectuer des opérations utiles sur plusieurs types de structure de manière interchangeable». Comment pourrait-on utiliser plusieurs structures membres du même nom? Si deux structures ont le même alignement de données et donc un membre avec le même nom et le même décalage que dans votre exemple, à partir de quelle structure puis-je produire les données réelles? (valeur). Deux structures ont le même alignement et les mêmes membres, mais des valeurs différentes sur eux. Pouvez-vous s'il vous plaît élaborer
Herdsman

@Herdsman: Dans les premières versions de C, un nom de membre struct encapsulait un type et un décalage. Deux membres de structures différentes peuvent porter le même nom si et seulement si leurs types et décalages correspondent. Si le membre struct fooest un intavec 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 anyPointeridentifié tout type de structure ayant foo
figuré

Avec le pointeur, vous pouvez déréférencer n'importe quelle adresse quelle que soit l'origine du pointeur, c'est vrai, mais alors quel est l'intérêt du compilateur pour contenir les tables des structures membres et leurs noms (comme vous l'avez dit dans votre article) si je peux récupérer des données avec n'importe quel pointeur connaissant simplement l'adresse d'un membre dans une structure particulière? Et si le compilateur ne sait pas si le anyPointeridentifie 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 matchedde votre publication?
Herdsman

@Herdsman: Le compilateur conserverait la liste des noms des membres de la structure car le comportement précis de p->foodépendrait du type et du décalage de foo. Essentiellement, p->fooc'é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.
supercat

0

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.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.