Licence CC 0

Les unions

Dernière mise à jour :

Dans la deuxième partie de ce cours nous vous avons présenté la notion d’agrégat qui recouvrait les tableaux et les structures. Toutefois, nous avons passé sous silence un dernier agrégat plus discret moins utilisé : les unions.

Définition

Une union est, à l’image d’une structure, un regroupement d’objet de type différents. La nuance, et elle est de taille, est qu’une union est un agrégat qui ne peut contenir qu’un seul de ses membres à la fois. Autrement dit, une union peut accueillir la valeur de n’importe lequel de ses membres, mais un seul à la fois.

Concernant la définition, elle est identique à celle d’une structure ci ce n’est que le mot-clé union est employé en lieu et place de struct.

1
2
3
4
5
6
7
union type
{
    int entier;
    double flottant;
    void *pointeur;
    char lettre;
};

Le code ci-dessus défini une union type pouvant contenir un objet de type int ou de type double ou de type pointeur sur void ou de type char. Cette possiblité de ne stocker qu’un objet à la fois est traduite par le résultat de l’opérateur sizeof.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#include <stdio.h>

union type
{
    int entier;
    double flottant;
    void *pointeur;
    char lettre;
};


int main(void)
{
    printf("%u.\n", sizeof (union type));
    return 0; 
}
1
8.

Dans notre cas, la taille de l’union correspond à la taille du plus grand type stocké à savoir les types void * et double qui font huit octets. Ceci traduit bien l’impossiblité de stocker plusieurs objets à la fois.

Notez que, comme les structures, les unions peuvent contenir des bits de bourrage, mais uniquement à leur fin.

Pour le surplus, une union s’utilise de la même manière qu’une structure et l’accès aux membres s’effectue à l’aide des opérateurs . et ->.

Utilisation

Étant donné leur singularité, les unions sont rarement employées. Leur principal intérêt est de réduire l’espace mémoire utilisé là où une structure ne le permet pas.

Par exemple, imaginez que vous souhaitiez construire une structure pouvant accueillir plusieurs types possibles, par exemple des entiers et des flottants. Vous aurez besoin de trois champs : un indiquant quel type est stocké dans la structure et deux permettant de stocker soit un entier soit un flottant.

1
2
3
4
5
6
7
struct nombre
{
    unsigned entier : 1;
    unsigned flottant : 1;
    int e;
    double f;
};

Toutefois, vous gaspiller ici de la mémoire puisque seul un des deux objets sera stockés. Une union est ici la bienvenue afin d’économiser de la mémoire.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
struct nombre
{
    unsigned entier : 1;
    unsigned flottant : 1;
    union
    {
        int e;
        double f;
    } u;
};

Le code suivant illustre l’utilisation de cette construction.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include <stdio.h>

struct nombre
{
    unsigned entier : 1;
    unsigned flottant : 1;
    union
    {
        int e;
        double f;
    } u;
};


static void affiche_nombre(struct nombre n)
{
    if (n.entier)
        printf("%d\n", n.u.e);
    else if (n.flottant)
        printf("%f\n", n.u.f);
}


int main(void)
{
    struct nombre a = { 0 };
    struct nombre b = { 0 };

    a.entier = 1;
    a.u.e = 10;
    b.flottant = 1;
    b.u.f = 10.56;

    affiche_nombre(a);
    affiche_nombre(b);
    return 0;
}
1
2
10
10.560000

Une autre utilisation fréquente des unions est de permettre de modifier l’alignement d’un objet ou, plus précisément, d’augmenter l’alignement d’un objet. En fait, il s’agit d’une des conséquences de l’union : étant donné qu’elle doit pouvoir stocker n’importe lequel de ses membres, son alignement doit être le plus élevé parmi celui de ses membres.

Pour rappel, l’alignement d’un type peut être connu à l’aide de la macrofonction offsetof() et d’un type structure de la forme struct { char c; type x; }.

Ainsi, si nous souhaitons aligner un objet de type int de la même manière qu’un objet de type double, il nous suffit de construire une union qui comprendra les deux types. Le code suivant vérifie ce qui vient d’être dit.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#include <stddef.h>
#include <stdio.h>

union nombre
{
    int e;
    double f;
};


int main(void)
{
    printf("int : %u\n", (unsigned)offsetof(struct { char c; int n; }, n));
    printf("union nombre : %u\n", (unsigned)offsetof(struct { char c; union nombre n; }, n));
    return 0;
}
1
2
int : 4
union nombre : 8

Dans la même veine, il est ainsi possible de connaître l’alignement le plus strict parmi les types natifs en construisant une union comportant les types les plus contraignants, à savoir : le type long, le type long double et le type void *.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
#include <stddef.h>
#include <stdio.h>

union align
{
    long e;
    long double f;
    void *p;
};


int main(void)
{
    printf("union align : %u\n", (unsigned)offsetof(struct { char c; union align n; }, n));
    return 0;
}
1
union align : 16

Comme pour l’exemple précédent, cette technique peut être utilisée pour imposer l’alignement le plus strict à un objet ayant une contraine d’alignement plus faible. Gardez bien ceci en mémoire, nous y reviendrons lors du T.P. final. ;)


En résumé
  1. Une union est un agrégat regroupant des objets de types différents, mais ne pouvant en stocker qu’un seul à la fois ;
  2. Comme les structures, les unions peuvent comprendre des bits de bourrage, mais uniquement à leur fin ;
  3. Une union acquiert l’alignement le plus strict parmi ceux de ses membres.