Champs de bits

L’unité usuelle de compte en informatique est l’octet, car les ordinateurs manipulent les données par paquets de huit bits en général, et souvent de seize ou plus. Cependant, dans certains cas, on doit accéder à certains bits individuellement dans une donnée.

Pour cela, on dispose d’opérateurs sur les types entiers, comme le décalage à gauche ou à droite (<< , >>), le « et » , le « ou » et le « ou exclusif » logiques (&, |, ^).

Cependant, lorsqu’on doit faire de nombreux accès aux bits séparés d’une donnée, cela devient trop long, et désagréable, de spécifier une opération logique chaque fois. Les structures à champs de bits permettent de résoudre ce problème.

Une telle structure est semblable à toute autre, mais derrière le nom des champs du type int ou unsigned, on précise un nombre de 1 à 16 indiquant la taille en bits du champ. Voici un exemple simple :

struct champbits {
     unsigned basbas     : 4;
     unsigned bashaut    : 4;
     unsigned hautbas    : 4;
     int      hauthaut   : 4;
     };

Cette structure occupe seize bits (4 fois 4) en mémoire, soit la taille d’un entier usuel. Notons qu’on aurait pu la déclarer plus brièvement ainsi :

struct champbits {
     unsigned basbas : 4, bashaut : 4, hautbas : 4;
     int    hauthaut : 4;
     };

Lorsqu’un champ de bits est unsigned, sa valeur varie de 0 à 2b -1, où b est le nombre de bits. Par exemple, le champ basbas a une valeur de 0 à 15. Lorsque le champ est signed, sa valeur varie de -2b-1à 2b-1-1, le bit de poids fort servant de bit de signe ; ainsi le champ hauthaut varie de -8 à 7.

Les champs de bits sont utilisés comme des entiers ordinaires ; lors d’une affectation, les bits excédentaires sont supprimés, les bits manquants sont nuls. Par exemple, si l’on écrit :

champbits cb;
int i = 30             // 30 == 0x1E
cb.bashaut = i;        // met 0xE == 14 dans cb.bashaut
i = cb.bashaut;        // maintenant i == 14

le résultat est de tronquer i en ne conservant que ses quatre bits de poids faibles.

Si vous écrivez  :

int i = -7890;
champbits cb;
cb = *(champbits*)&i;        // recopie i dans cb

vous obtiendrez dans cb la décomposition de -7890 en quatre parties, soit { 14, 2, 1, -2 }, indiquant que ce nombre vaut 0xE12E en mémoire (dans sa forme sans signe).

Voici un autre exemple, un peu plus intéressant à notre avis. La fonction suivante calcule la valeur de la mantisse, de l’exposant et du signe d’un nombre à virgule flottante float. Ces quantités sont réparties ainsi dans les quatre octets occupés par un float (des bits de poids faibles aux forts) : 23 bits de mantisse, 8 bits d’exposant (biaisé par 127), et un bit de signe :

void disseque(float f, int& signe,  int& exposant   long& mantisse)
{
     struct dissq {
         unsigned mantisse1 : 16;
         unsigned mantisse2 : 7;
         unsigned exposant  : 8;
         int signe : 1;
     } fb;

     fb = *(dissq*)&f;        // recopie   f dans fb
     exposant = fb.exposant -127;
     signe = fb.signe;
     mantisse = 0x800000 | fb.mantisse1  |
                (long(fb.mantisse2) << 16);
}

L’exposant est biaisé, ce qui explique qu’il faille retirer 127. Quant à la mantisse, elle est ici en deux parties car on ne peut avoir de champs de bits de plus de seize bits. En outre, le bit le plus élevé (le vingt-quatrième), qui est toujours à 1, n’est pas stocké dans le nombre, il faut le rajouter explicitement (d’où le 0x800000). La valeur du nombre est alors :

formule

Nous avons vu précédemment que tous les bits des champs de bits étaient placés les uns derrière les autres, du moins significatif (déclaré en premier) au plus significatif. Il arrive que l’on ne souhaite pas utiliser certains bits. Dans ce cas, il suffit de ne pas nommer le champ correspondant. Par exemple, les microprocesseurs de la famille 8086 ont un mot d’état de seize bits, dont seuls quelques-uns ont un sens. Ainsi, le bit ZF est à 1 si la dernière opération a produit un résultat nul, à 0 sinon. Voici une structure reproduisant cette configuration :

struct flags {
     unsigned CF : 1;    // retenue
     unsigned : 1;    
     unsigned PF : 1;    // parité
     unsigned : 1;
     unsigned AF : 1;    // retenue auxiliaire
     unsigned : 1;
     unsigned ZF : 1;    // zéro
     unsigned SF : 1;    // signe
     unsigned TF : 1;    // trap
     unsigned IF : 1;    // autorisation d’interruption
     unsigned DF : 1;    // direction
     unsigned OF : 1;    // débordement
     unsigned : 4;
     }

Les bits non utilisés, au nombre de sept, figurent sans nom dans cette structure.

Signalons aussi que, lorsque l’on demande au compilateur Turbo C++ d’aligner les données sur les mots de mémoire, il ne doit pas y avoir de champ de bits chevauchant une limite de mot, sinon il est décalé sur le mot suivant.

On notera que les champs de bits n’ont pas d’adresse mémoire (il est illégal d’utiliser l’opérateur d’adressage & avec eux), puisqu’ils ne se trouvent pas nécessairement sur une limite d’octet. En outre le langage ne permet pas de les organiser en tableaux.

Les champs de bits peuvent procurer des facilités dans certains cas ; ils sont surtout utiles dans des applications très techniques faisant intervenir le matériel ou les périphériques.

Une structure peut avoir à la fois des champs de bits et des champs normaux, ainsi que des méthodes. Une classe et une union (ci-après) peuvent aussi en avoir.

Précédent Précédent Sommaire Sommaire Suivant Suivant