Enumérations. Types non scalaires
(union, struct et enum).
Définitions de nouveaux types
Dernière mise à jour : 15 février 2010


Rappels : les types de base

Rappelons que l'opérateur sizeof appliqué à un type ou une variable donne la taille de l'espace occupé en mémoire exprimée en nombre d'octets.
Le nombre associé est de type long int. Le traitement de tous les sizeof est réalisé dès la compilation : cela impose que les types utilisés soient suffisamment définis pour que cette taille soit calculable. L'utilisation de sizeof ne donne lieu à aucun appel lors de l'exécution du programme.
Il est donc possible d'utiliser sizeof dans des (utilisables dans un switch) ou pour définir un tableau.

Rappel sur les types scalaires de base du langage C :


Conversions de types ou transtypages)

Introduction

▶ Lorsque dans une expression arithmétique on mélange le type des opérandes, les données de type plus petit sont automatiquement convertis en le type le plus grand : ainsi lorsqu'on ajoute un int et un float, l'entier est automatiquement tranformé en flottant.

▶ Dans certaines opérations, le type des opérandes est essentiel et conditionne la nature des opérations et des résultats.
Un exemple simple en est la division : appliqué à deux opérandes entiers, l'opérateur de division / réalise une division euclidienne (entière.
Ainsi si x et y sont des variables entières de valeurs respectives 12 et 5, l'expression x/y a pour valeur 2.
Si on veut obtenir une division réelle, il faut transformer au moins l'un des deux opérandes en une grandeur de type float.

Conversion implicite vs. conversion explicite

On peut distinguer deux types de conversion de types :

les conversions implicites : elles sont réalisées de manière automatique sans provoquer d'erreur lorsque dans une expression des opérandes de types différents sont utilisés.
Les règles utilisées sont dans l'ordre les suivantes :

  • si l'un des opérandes est de type double, l'autre est converti en double
  • si l'un des opérandes est de type float, l'autre est converti en float
  • les opérandes de type char ou short sont convertis en int
  • si l'un des opérandes est de type long, l'autre est converti en long

les conversions explicites : elles sont réalisées à la demande explicite du programmeur au travers d'un cast (ou coercition). Ainsi :

  • si x est une variable de type int, « (float)  » est la valeur de x vue comme un nombre de type float (et donc représentée comme tel en interne);
  • si x et y sont des variables de type int, un expression telle que « (float) x / y » calcule la division réelle des valeurs des deux variables;
  • si ptr1 et ptr2 sont respectivement des pointeurs sur les types T1 et T2, il est possible d'écrire

            ptr1 = (T1 *) ptr2;

    pour affecter la valeur de ptr2 à ptr1 et donc voir le contenu de la zone pointée par ptr2 comme une grandeur de type T1.


Les valeurs booléennes

Il n'y a pas de type booléen en langage C.

▶ Toute expression arithmétique (à valeur entière) possède une valeur logique/booléenne :

  • la valeur 0 correspond à la valeur logique « FAUX »
  • tout ce qui n'a pas comme valeur 0 a comme valeur logique « VRAI »
▶ La valeur « VRAI » par défaut est l'entier 1 (valeur affectée à un entier dont la valeur est le résultat d'un test vrai).

▶ L'exemple suivant illustre quelques aspects de ce mode particulier de traitement du calcul booléen en langage C et les liens qui y existent entre entiers et booléens :

    --> cat booleen.c
    #include <stdio.h>
    void main( ) { // on suppose que le binaire s'appelle booleen
       int x = 5, y = 7;
       int z = x < y;
       printf("valeur de %d < %d : %d\n", x, y, z);
       printf("non z = %d\n", !z);
       z = x > y;
       printf("valeur de %d > %d : %d\n", x, y, z);
       printf("non z = %d\n", !z);
       printf("non %d = %d\n", x, !x);
    if (x) // test équivalent à (x != 0)
       printf("vrai\n");
    else
       printf("faux\n");
       return EXIT_SUCCESS; // return 0; }
    --> ./booleen
    valeur de 5 < 7 : 1
    non z = 0
    valeur de 5 > 7 : 0
    non z = 1
    non 5 = 0
    vrai
    -->


Les énumérations

▶ Un type énumération permet de définir des listes de noms symboliques correspondant à des valeurs entières.

▶ Tout type construit sur ce modèle est de fait une « copie » du type int dans laquelle un certain nombre de constantes symboliques ont été définies par exemple à des fins de lisibilité des applications. Définir une énumération ne définit donc pas un domaine de définition mais simplement quelques constantes symboliques pour des entiers (un entier peut d'ailleurs posséder plusieus constantes symboliques dans une même énumération).

▶ Ce type de défintions est une alternative à la définition de constantes avec la directive #define du préprocesseur C.

Exemple 1

Dans ce premier exemple, qui permet d'introduire la syntaxe utilisée on définit le type booleen comme énumération dans laquelle deux constantes nommées faux et vrai sont définies :
    --> cat enum1.c
    #include <stdio.h>
    enum booleen { // l'énumération se utilisable via « enum booleen »
       faux, // faux est implicitement un nom symbolique pour l'entier 0
       true, // true est implicitement un nom symbolique pour l'entier 1
       vrai = 1 // vrai est explicitement un nom symbolique pour l'entier 1
       false = vrai - 1
    };
    int main( ) {
       enum booleen b1, b2, b3; // déf. de 3 variables du type « enum booleen »
       printf("%d\n", false);
       b1 = faux;
       if(b1 == vrai) printf("oui\n");
       else printf("non\n");
       b2 = b1 + 1;
       printf("valeur de b2 : %d\n", b2);
       if(b2 == true && b2 == vrai) printf("oui\n");
       else printf("non\n");
       b3 = 10;
       printf("valeur de b3 : %d\n", b3);
       b3 = -20 + b1 + b2;
       printf("valeur de b3 : %d\n", b3);
       return EXIT_SUCCESS; // return 0;
    }
    --> ./enum1
    0
    non
    valeur de b2 : 1
    oui
    valeur de b3 : 10
    valeur de b3 : -19
    -->

Exemple 2

Ce deuxième exemple montre que définir une énumération définit un ensemble de constantes symboliques plutôt qu'un type.
Ces constantes peuvent être utilisées en lieu et place d'entiers (une telle définition est équivalente à une séquence de #define.
Une même constante ne peut pas être définie plusieurs fois (même dans des énumérations différentes).
    --> cat enum2.c
    #include <stdio.h>
    enum e1 {
       bleu = 0, // #define bleu 0
       blanc = 1, // #define blanc 1
       rouge = 2 // #define rouge 2
    };
    enum e2 {
       vert = 0, // #define vert 0
       jaune = 1, // #define jaune 1
       // bleu = 3, provoquerait une erreur d'ambiguité
       gris = 4 // #define gris 4
    };
    int n;
    enum e1 c1;
    enum e2 c2;
    int main( ) {
       n = jaune;
       c1 = blanc;
       c2 = c1;
       c1 = gris;
       printf ("c1 = %d\n", c1);
       printf ("c2 = %d\n", c2);
       printf ("n = %d\n", n);
       return 1;
    }
    --> ./enum2
    c1 = 4
    c2 = 1
    n = 1
    -->


Les structures

Définition d'une structure

▶ Il s'agit de pouvoir regrouper dans une seule entité des valeurs de types éventuellement différents et de pouvoir accéder à chacun de ces constituants (appelés champs ou membres) individuellement.

▶ La description de la structure est

▶ Des exemples de définitions de structures sont données ci-après:

▶ Il est évidemment possible de créer des alias sur une structure en utilisant typedef comme dans :
   typedef
       struct {
          int a, b; // a et b dans la même déclaration
       } couple;

Déclaration de variables associées à une structure donnée

▶ La déclaration d'une variable cpl ayant la structure couple_entiers définie précédemment :
     struct couple_entiers cpl;

▶ Une telle déclaration aurait pu être réalisée en même temps que la déclaration de la structure elle-même :
     struct couple_entiers {int a, b;} cpl;

Il est important de noter, dès à présent, que lorsqu'on définit une variable dont le type est une structure, il lui correspond en mémoire un espace de la taille de la structure. Ceci est différent de ce qui ce passe par exemple en Java où, lorsqu'on déclare une variable dont le type est une classe, il n'est pas alloué d'espace pour un objet de ce type, mais l'espace pour une référence vers un tel objet. Nous y reviendrons plus loin.

▶ Plusieurs variables peuvent être définies simultanément.
Dans l'exemple suivant, on définit deux variables adr1 et adr2 ayant la structure adresse et une variable ptr_adresse de type « pointeur sur adresse » :
     struct adresse adr1, adr2, *ptr_adresse;

Initialisation d'une variable ayant une structure donnée

Elle peut être réalisée :

Opérations possibles


     struct couple_entiers {int a, b;} cpl1 = {2}; /* le champ de nom a vaut 2, le champ n'est pas initialisé */
     struct couple_entiers cpl2 = {4, 5}; /* les champs a et b sont initialisés respectivement à 3 et 4 */

Exemples

▶ Une structure correspondant à un nombre complexe dont la partie réelle et la partie imaginaire sont de type double  et la définition de deux variables c1 et c2 ayant cette structure (s'agissant d'une définition l'espace mémoire nécessaire à chaque variable est alloué) :

   struct complexe {
      double reel;
      double imag;
   } c1, c2;
    ..........
   c1.reel = 1.13;
   c1.imag = -2.25;
   c2 = c1; // copie physique : c2.reel=c1.reel; c2.image=c1.imag;
    ..........
   struct complexe c3 = {21.5, 4}; // c3.reel = 21.5; c3.imag = 4;

Taille d'une structure

La taille d'une structure est a priori égale à la somme des tailles des champs qui la constituent.

Ainsi, on a pour la structure complexe définie ci-dessus :

Dans cet autre exemple, utilisant pour l'un de ses champs, le type couleur définie comme une énumération, la structure ainsi définie :
    struct point_colore {
       float abs, ord;
       enum { // énumération anonyme
          blanc = 1,
          bleu = 3,
          rouge,
          noir = 10
       } couleur; // champ couleur du type enum définie localement
       int intensite;
    };

a comme taille 16 (sizeof(int) + 2 * sizeof(float) + sizeof(couleur), chacune de ces tailles étant égale à 4).

Cependant les choses peuvent ne pas être aussi simples.
Si on considère les trois structures st1, st2 et st3 suivantes :

    struct st1 {
       char a;
       int distance;
       char b;
       couleur couleur;
       };
    struct st2 {
       char a;
       char b;
       int distance;
       couleur couleur;
    };
    struct st3 {
       char a;
       char b;
       char c;
       char d;
       int distance;
       couleur couleur;
    };

on a pu observer que :

L'explication en est dans le problème de l'alignement des adresses.

Les structures anonymes

Ainsi que nous l'avons dit, il est possible de nommer une structure sans lui de donner de nom. Cela n'a d'intérêt que si on y déclare instantanément des variables.
Il est important de noter que des variables définies dans deux définitions identiques d'une structure ne pas considérées comme étant de même type, ce qu'illustre l'exemple suivant :

    --> cat structAnonyme.c
    #include <stdio.h>
    struct {int a, b;} c1 = {3}, c2 = {4, 5}, c3;
    struct {int a, b;} c4;
    void main( ) { // on suppose que le binaire s'appelle booleen
       c3 = c2; /* affectation légale : variables de même type */
       printf("%d %d\n", c3.a, c3.b);
       c4 = c1; /* affectation illégale : variables de type différents */
       return 0;
    }
    --> gcc structAnonyme.c
    structAnonyme.c: In function 'main':
    structAnonyme.c:7: error: incompatible types in assignment
    -->

Les unions

Définition

Alors que les structures permettent la concaténation d'informations, les unions visent en quelque sorte à en permettre la superposition. Une union permet ainsi d'interpréter de différente manière un même emplacement en mémoire.
Du point de vue syntaxique, la définition ou l'utilisation d'une union obéit aux mêmes règles que les structures, le mot clé union remplaçant le mot clé struct.

Dans l'exemple suivant, on définit une union dont la définition reflète le fait qu'un mot de la mémoire d'un ordinateur (qui n'est jamais qu'une suite d'un certain nombre [par exemple 32] de bits) est interpétable comme un nombre entier, une suite de deux entiers courts (ces grandeurs pouvant être ou non signées), un flottant, un tableau de 4 caractères (signés ou non), voire comme une instruction machine.

    union mot {
       signed char car[4];
       unsigned char ucar[4];
       short sh[2];
       unsigned short ush[2];
       int ent;
       unsigned int uent;
       float reel;
       struct instruction inst; // supposée définie par ailleurs
   };

La taille d'une union est égale à la taille de son membre le plus « long » (les différents membres ne sont pas nécessairement tous de même taille).
Ainsi en supposant que la taille de la structure inst soit 8, la taille de l'union mot est 8.

Accès aux membres constituant une union

Une union permet d'avoir des vues différentes d'un même espace (ses membres).
L'accès à l'un de ces membres est réalisé avec l'opérateur « . »

En reprenant la définition de l'union mot précédente, dans la séquence suivante, on :

  • déclare une variable m de ce type;
  • on initialise l'espace qui lui correspond en affectant à l'un de ses membres (ici le membre de type int) une valeur du type correspondant;
  • on affiche l'interpétation de ce contenu selon les différentes autres vues (accédées via les autres membres de l'union).
    ..........
    union mot m ;
    m.ent = 0x6789ABCD; // binaire 01100111100010011010101111001101
    printf("m.ent = %d\n", m.ent);
    printf("m.reel : %f\n", m.reel);
    printf("m.sh : %d [%x] %d [%x]\n",
              m.sh[0], m.sh[0], m.sh[1], m.sh[1]);
    printf("m.ush : %d [%x] %d [%x]\n",
              m.ush[0], m.ush[0], m.ush[1], m.ush[1]);
    printf("m.car : %d %d %d %d\n",
              m.car[0], m.car[1], m.car[2], m.car[3]);
    printf("m.ucar : %d %d %d %d\n",
              m.ucar[0], m.ucar[1], m.ucar[2], m.ucar[3]);
    ...........

Son exécution donne :
    m.ent = 1737075661 // binaire 01100111100010011010101111001101
    m.reel : 1300266746393047005659136.000000
    m.sh : -21555 [ffffabcd] 26505 [6789]
    m.ush : 43981 [abcd] 26505 [6789]
    m.car : -51 -85 -119 103
    m.ucar : 205 171 137 103

Ces résultats résultent de la représentation interne des types des membres de cette union (rappels).