Le langage C

Cinquième partie

par Guilhem de Wailly

Résumé

Dans cette série d'articles, nous présentons le langage C. Il ne s'agit pas de réécrire les ouvrages de référence donnés en annexe, mais de donner, au travers du C, une méthode de programmation basée sur la notion d'interface.

Au fur et à mesure de notre progression, nous décrirons les éléments indispensables du langage et conduirons le lecteur à consulter les ouvrages de références. Le but poursuivi est clairement de parvenir à construire des programmes de manière segmentée.

Dans cet article, nous introduisons la manière de définir de nouveaux types de données et nous utilisons immédiatement nous connaissance pour entamer la définition d'une bibliothèque manipulant des nombres rationnels.

Enumération

Lorsque l'on programme, on a souvent à faire à des variables qui ne peuvent contenir qu'un certain nombre d'éléments délimité.

Par exemple, nous pourrions avoir une variable de type fruit, pouvant contenir l'une des valeurs pomme, poire, fraise ou mure.

Le langage C permet de définir de tels types de données appelées énumération. Pour cela, nous écrivons :

        enum fruit {
          pomme,
          poire,
          fraise,
          mure
        };

        void f (enum fruit x) {
          if (x == pomme) printf ("pomme \n");
          else if (x == poire ) printf ("poire \n");
          else if (x == fraise) printf ("fraise\n");
          else if (x == mure  ) printf ("mure  \n");
        }

        void main (void) {
          f (fraise);
        }
      

Ce programme commence par définir une énumération nommée fruit. Cette énumération contient les noms de fruit. Les mots pomme, poire, fraise et mure sont les valeurs possibles pour une variable de type enum fruit.

Puis il définit une fonction dont x le paramètre est de type enum fruit. Cette fonction effectue un certain nombre de tests destinés à afficher le nom de fruit correspondant.

Enfin, pour utiliser cette fonction f, lafonction main est définie. Elle invoque f avec l'argument fraise.

Dans cet exemple, nous voyons que les mots enum fruit sont devenus un nouveau type de donnée, comme int est le type de donnée des entiers.

Il serait possible de se passer de l'énumération en utilisant par exemple des nombres entiers. L'énumération est importante car elle permet au compilateur de vérifier que les valeurs affectés aux variables appartiennent bien aux éléments de l'énumération. De plus, les programmes sont plus lisibles.

Dans la fonction f, nous avons utilisé une série de test pour afficher le nom de fruit de x. Le C propose dans ce cas, une instruction d'énumération appelée switch.

Instruction de sélection

Lorsque nous souhaitons sélectionner des fragments de programmes en fonction de la valeur d'une expression, le C met à notre disposition l'instruction switch. La syntaxe générale du switch est la suivante :

        switch (expression) {
          case valeur1:
            expressions1;

          case valeur2:
            expressions2;
          ...
          case valeurn:
            expresisonsn;

          default:
            expresisonsd;
        }
      

L'expression est n'importe quelle expression C. Les valeuri sont des constantes, comme 1, 2, 3, ou pomme, poire. Les expressionsi sont une ou plusieurs expressions C, séparées par des points virgules.

La clause default est facultative. Elle n'est pas obligatoirement placée à la fin du switch.

Le switch évalue expression. Puis il cherche parmi les clauses case celle dont la valeur correspond la valeur retournée par l'évaluation précédente.

Si aucune clause ne correspond, la clause default est sélectionnée et les expressionsd sont exécutées. Si la clause default n'existe pas, aucune expression n'est exécutée.

Si une clause correspond, alors les expressions attachées à la clause sont exécutées, AINSI QUE TOUTES LES EXPRESSIONS SUIVANTES. Cela est une différence avec la plupart des autres langages proposant la sélection et c'est une cause fréquente de bugs dans les programmes.

Si on souhaite n'exécuter que les expressions correspondantes à la clause sélectionnée, il faut explicitement "sortir du switch" en utilisant l'instruction break.

La fonction f d'affichage des noms de fruits vue dans la section précédente devient en utilisant un switch :

        switch   (x) {
        case pomme:
          printf ("pomme \n");
          break;
        
        case poire:
          printf ("poire\n");
          break;
        
        case fraise:
          printf ("fraise\n");
          break;
        
        case mure:
          printf ("mure  \n");
          break;
        }
      

Les breaks sont nécessaires pour ne pas afficher les noms en dessous de celui sélectionné. Sans eux, et si x vaut fraise, on verrait affiché à l'écran, sur deux lignes, fraise et mure.

Les valeurs après les cases doivent être des constantes, comme des nombres, des caractères, des valeurs d'énumération. Elles ne doivent pas nécessiter une évaluation pour obtenir leur valeur.

Structure de donnée

Les énumérations permettent de restreindre le nombre de valeurs possibles pour une variable.

Le langage C permet aussi de définir des types de données composés, c'est à dire contenant des champs, chacun ayant un certain type.

Ce type de données est appelé structure et répond à la syntaxe générale suivante :

        struct nom {
          type1 nom1;
          type2 nom2;
          ...
          typen nomn;
        };
      

Les typei sont soit des types de données standards, soit des types que nous avons définis.

Les nomi sont les noms des champs. Par exemple, on pourrait définir un type de donnée dont le premier champ serait un fruit et le second un entier représentant le mois ou le fruit est mûr :

        struct fruit_mois {
          enum fruit nom;
          int  mois;
        };
      

Pour déclarer une variable structure, nous procédons comme avec l'énumération :

        struct fruit_mois f;
      

Nous ne pouvons pas accéder globalement à tous les champs de la structure. Aussi le langage C définit une syntaxe pour accéder individuellement aux champs de la structure :

        f.nom = pomme;
        f.mois = 9;
      

Il suffit de séparer la variable structure du nom de champ par un point. Si un champ est aussi uns structure, on peut placer plusieurs point, comme dans :

        s.champ_1.champ_123.variable = 123;
      

Création de types de données

Dans ce qui précède, nous avons vu que le langage C permet de construire de nouveaux types de donnée.

Mais nous ne sommes qu'à moitié satisfaits car il nous faut toujours, au moment de la déclaration d'une variable, indiquer la nature du type, comme avec :

        enum fruit variable;
      

Nous aimerions pouvoir déclarer une variable avec un nom de type sans indiquer la nature du type de donnée, comme avec :

        fruit variable;
      

Les auteurs du langage C avaient pensé à tout ! Pour cela, ils ont défini le mot clef typedef qui permet de définir de nouveaux types de données dont on ne peut faire la différence d'avec les types standards.

Si nous écrivons :

        enum fruit variable;
      

nous déclarons une nouvelle variable de type enum fruit. Si nous plaçons devant toute l'expression le mot clef typedef, nous déclarons alors un nouveau type de donnée :

        typedef enum fruit type;
      

Maintenant, enum fruit et type sont équivalents, et nous pouvons écrire indifféremment :

        enum fruit x;
      

ou :

        type x;
      

Le langage C nous permet de rassembler les deux déclarations suivantes :

        enum fruit {
          pomme,
          poire,
          fraise,
          mure
        };

        typedef enum fruit fruit;
      

En une seule écriture compacte, que nous utiliserons toujours par la suite :

        typedef enum fruit {
          pomme,
          poire,
          fraise,
          mure
        } fruit;
      

Pour une structure de donnée, cela donne :

        typedef struct nom {
          type1 nom1;
          type2 nom2;
          ...
          typen nomn;
        } nom;
      

Ce qui nous permet de déclarer une variable x de type nom avec :

        nom x;
      

Maintenant, nous allons pouvoir nommer nos nouveaux types de données de manière complètement transparente.

Nombres rationnels : primitives

Forts de nos nouvelles connaissances, nous souhaitons concevoir une bibliothèque pour les nombres rationnels.

Nous allons concevoir notre bibliothèque comme une véritable librairie C orientée objet : l'objet rationnel aura un constructeur et un destructeur, et chaque champ de l'objet aura un sélecteur pour y accéder.

Structure de donnée

La première chose à faire est de définir le type de donnée :

        typedef struct rationnel {
          int num;
          int den;
        } rationnel;
      

Cette structure contient deux champs de type entier int ; le premier champ est le numérateur et le second champs est le dénominateur.

Constructeur et destructeur

La première fonction que nous allons définir permet la construction d'un nombre rationnel. On passe à cette fonction deux entiers et elle retourne un nombre rationnel :

        rationnel r_construit (int num, int den) {
          rationnel r;
          
          r.den = den;
          r.num = num;
          
          return (r);
        }
      

La seconde fonction à définir est le destructeur. Dans le cas des rationnels, cette fonction ne fait rien. Mais dans d'autres cas, il pourrait y avoir de la mémoire à libérer ou des fichiers à fermer. Pour proposer un canevas général, même si la fonction ne fait rien, il faudra la définir et l'utiliser. La fonction de destruction est :

        void r_detruit (rationnel x) {
          return;
        }
      

L'instruction return n'est ici pas utile. Mais nous l'écrivons quand même pour bien spécifier notre intention de quitter immédiatement la fonction sans rien faire.

Sélecteurs

Les sélecteurs sont des fonctions qui permettent d'accéder aux champs de la structure. Elles s'écrivent simplement :

        int r_den (rationnel x) {
          return (x.den);
        }

        int r_num (rationnel x) {
          return (x.num);
        }
      

Nombre rationnels : utilitaires

Maintenant que nous avons les primitives de manipulation des nombres rationnels, nous pouvons créer des fonctions de manipulation.

Addition

La première fonction permet de d'additionner deux nombres rationnels et de retourner le résultat sous la forme d'un nombre rationnel :

        rationnel r_plus (rationnel x, 
                          rationnel y) {
                          rationnel r;
          int n, d;
          
          n =   r_num (x) * r_den (y)
              + r_num (y) * r_den (x);
          d =   r_den (x) * r_den (y);
          
          r =   r_construit (n, d);
          
          return (r);
        }
      

Nous rappelons que :

        a/b + u/v = (av + ub)/(bv).
      

Affichage

Cette fonction affiche un nombre rationnel à l'écran :

        void r_affiche (rationnel x) {
          printf ("%d/%d", 
          r_num (x),
          r_den (x));
        };
      

Nombre rationnels : utilisation

Nous avons maintenant de quoi réaliser un petit programme à base de nombres rationnels :

        void main (void) {
          /* déclarations */
          rationnel u, v, w;
        
          /* construction */
          u = r_construit (12, 31);
          v = r_construit (7, 3);
          
          /* addition */
          w = r_plus (u, v);
          
          /* affichage */
          r_affiche (u);
          printf (" + ");
          r_affiche (v);
          printf (" = ");
          r_affiche (w);
          
          /* destruction */
          r_detruit (u);
          r_detruit (v);
          r_detruit (w);
        }
      

Ce programme créer deux nombres rationnels u et v et place le résultat de leur addition dans le nombre rationnel w.

Un message est alors affiché, rendant compte de l'opération. Puis tous les nombres rationnels créés sont détruits.

Paquetage

Pour faire fonctionner ce programme, il faut rassembler toutes les fonctions dans le même fichier, placer #include <stdio.h> en tête du fichier (nous utilisons printf), le compiler, et exécuter le binaire produit.

Examinons la structure du programme. La première partie est regroupée dans une section appelée primitive. Cette section définit le type de donnée, le constructeur, le destructeur et les sélecteurs.

La seconde section se nomme utilitaires : elle regroupe des fonctions qui n'utilisent que le nom du type de donnée et les fonctions définis dans la section primitive.

Enfin, la troisième partie utilise ce qui défini dans les deux sections primitive et utilitaire.

Cette structure peut sembler lourde. Elle à l'avantage d'être complètement générique et l'on peut concevoir des programmes de taille très importante en suivant ce modèle.

Il est intéressant de constater que les fonctions des sections utilitaires et utilisation ne font absolument aucune hypothèse sur la nature du type de donnée quelles manipulent. Elles manipulent un nom, rationnel, avec des fonctions. Elles ne connaissent rien de rationnel, hormis son nom et les fonctions.

Ceci est la base de la notion de paquetage : un paquetage fournis des fonctionnalités agissant sur des noms de données, sans jamais rien dévoiler de la manière de les réaliser.

Ainsi, la réalisation est devenue complètement indépendante. Nous verrons que notre manière de réaliser les nombres rationnels est très pratique et simple, mais absolument inefficace.

Nous serons amenés à modifier complètement la manière de réaliser les nombres rationnels, et nous constaterons que seule la section primitive sera modifiée, les autres sections restantes inchangées. Ainsi, sur de gros programmes, seule un toute petite partie devrait être modifiée, ce qui entraîne automatiquement une réduction importante des coûts de développement.

Dans les articles qui vont suivrent, nous allons améliorer notre paquetage et créant une véritable interface "opaque" où les nombres rationnels seront créés de manière dynamique dans la mémoire. Cela nous conduira à créer des programmes sur plusieurs fichiers, et donc à examiner la gestion de projets sous Linux.

L'auteur

Guilhem de Wailly (gdw at free dot fr)

Références