Version en ligne

Tutoriel : Les listes doublement chaînées: de la théorie à la pratique

Table des matières

Les listes doublement chaînées: de la théorie à la pratique
Le besoin
Réfléchir avant d'agir
Avant de commencer
Correction
Améliorations

Les listes doublement chaînées: de la théorie à la pratique

Le besoin

Après avoir vu la théorie sur les listes doublement chaînées, nous allons mettre en oeuvre nos connaissances sur un petit sujet. Pour suivre ce tutoriel, la lecture du premier tutoriel sur les listes doublement chaînées est indispensable, sans quoi vous risqueriez de ne pas comprendre les concepts mis en jeu.
Les objectifs de ce tutoriel sont les suivants :

Allez, en route.

Le besoin

Réfléchir avant d'agir

La réalisation d'un projet informatique doit suivre plusieurs étapes. La première de ces étapes est l'étape de spécification. Ici, il s'agit de répondre à la question QUOI ?. On parle aussi de cahier des charges. Nous allons donc dans un premier temps définir en détails notre besoin. Lors de la réalisation d'un projet, il est primordial de savoir ce que l'on veut réaliser, sans quoi on risque d'aller droit dans le mur.

Le projet

De nos jours, de nombreuses entreprises ont à manipuler des pièces afin de par exemple les assembler. Avant le travail était manuel, désormais il est devenu automatisé. Différentes chaînes de montage sont alors mises en place dans les ateliers afin de manipuler et véhiculer les différentes pièces d'un atelier à un autre. Pour véhiculer les pièces, des bras manipulateurs ont alors été conçus. Ceux-ci constituent ce que l'on appelle un système automatisé. En effet, ces bras manipulateurs sont capables d'agir par eux-mêmes, sans pour autant avoir besoin d'un pilotage quelconque. Il suffit alors de paramétrer les mouvements que doit effectuer le bras et le tour est joué.
Dans ce tutoriel, nous nous proposons de mettre en place un système de simulation de bras automatisé. Dans le cadre du tutoriel, nous nous contenterons de prendre un bras minimaliste. En effet, si lors de vos études vous vous êtes déjà rendus dans un laboratoire de sciences de l'ingénieur, vous avez peut-être eu l'occasion de voir un bras manipulateur capable d'effectuer certains mouvements, pilotable directement par automate programmable. Ce bras porte le nom de Bras Schrader.

Ce que l'on souhaite réaliser

Afin de mettre en oeuvre le concept des listes doublement chaînées, nous souhaitons réaliser un programme de simulation de bras automatisé. Ce programme sera effectué sous forme d'un interpréteur de commandes (shell). Nous aurons donc l'occasion d'apprendre à réaliser un interpréteur de commandes minimaliste. Cet interpréteur de commandes permettra alors à l'utilisateur de contrôler le bras afin de lui faire effectuer certains mouvements. Le bras que nous souhaitons simuler dispose d'une base rotative ainsi que d'une pince permettant de saisir un objet de petite taille. Les mouvements pouvant être effectués par le bras sont les suivants :

L'interpréteur de commandes

Chaque mouvement doit disposer d'une commande permettant de l'effectuer. D'autre part, la casse doit être prise en compte. Une commande écrite en minuscules sera considérée comme invalide. Ainsi, afin de paramétrer un mouvement au bras, l'utilisateur devra taper une certaine commande. Les commandes dont on dispose sont les suivantes :

Nom de la commande

Rôle de la commande

UP

Montée du bras

DOWN

Descente du bras

ROTL

Rotation du bras vers la gauche

ROTR

Rotation du bras vers la droite

FORWARD

Avancée de la pince

BACKWARD

Recul de la pince

OPEN

Ouverture de la pince

CLOSE

Fermeture de la pince

Un mouvement ne peut être effectué deux fois de suite. Ainsi, lors de la saisie de deux commandes identiques d'affilée, un avertissement sera affiché.
Une fois le mouvement du bras paramétré, l'utilisateur devra saisir la commande VIEW afin de visualiser le mouvement sur la sortie standard. La visualisation doit se faire sous forme de texte. La commande REINIT quant à elle permet de replacer le bras en position par défaut, à savoir en position basse intermédiaire. Enfin, la commande HELP doit permettre à l'utilisateur de visualiser la liste de toutes les commandes ainsi que leur description. Pour quitter la console, l'utilisateur devra saisir la commande EXIT.

L'affichage

Afin de visualiser le résultat du mouvement paramétré, l'utilisateur devra saisir la commande VIEW. Une fois celle-ci saisie, l'affichage du mouvement final doit être réalisé sur la sortie standard (à savoir stdout). Pour chaque commande, deux informations
devront s'afficher à l'écran :

Pour faire la différence, toute action devra débuter par un point '.' et toute transition devra débuter par un tiret '-'. Afin de s'approcher le plus possible d'un comportement réel, une temporisation devra être mise en place entre chaque mouvement et transition, et entre chaque transition et mouvement. Par exemple, le programme devra attendre 0.5 seconde avant d'afficher la transition associée au mouvement, et 1 seconde avant d'afficher le mouvement suivant.

Exemple

Voici un exemple de comportement :

Bras:> UP
Ajout commande UP (Levee du bras)
Bras:> UP
Attention: commande UP deja realisee
Bras:> COME
Erreur: commande 'COME' inconnue
Bras:> ROTR
Ajout commande ROTR (Rotation droite)
Bras:> FORWARD
Ajout commande FORWARD (Avancee de la pince)
Bras:> OPEN 
Ajout commande OPEN (Ouverture de la pince)
Bras:> CLOSE
Ajout commande CLOSE (Fermeture de la pince)
Bras:> VIEW
Visualisation en mode pas a pas 
- Levee du bras
. Bras en position haute
- Rotation droite
. Bras en butee droite
- Avancee de la pince
. Pince avancee
- Ouverture de la pince
. Pince ouverte
- Fermeture de la pince
. Pince fermee
Fin de visualisation
Bras:> REINIT
Bras en position par defaut
Bras:> EXIT

Réfléchir avant d'agir

Réfléchir avant d'agir

Le besoin Avant de commencer

Lorsque l'on est confronté à un problème bien défini, il est nécessaire de savoir comment on va le résoudre. Cette phase de l'élaboration d'un projet informatique est appelée phase de conception. Il s'agit alors de répondre à la question COMMENT ? après avoir répondu à la question QUOI ?. Cette étape est tout autant primordiale que la première. En effet, dans un projet informatique, la phase de codage n'est qu'une toute petite étape. Si l'on se lance directement dans le code tête baissée, on risque de se cogner sur un mur. Plus le projet est gros et plus le risque est important. 'Réfléchir avant d'agir' est ici l'expression clé.
Pour nous, le but de cette étape est d'établir les points suivants :

Commençons alors par réfléchir aux différents fichiers dont nous aurons besoin.
Tout d'abord, et comme la coutume le veut, le point d'entrée du programme (à savoir la fonction main), sera placé dans le fichier main.c.
Ensuite, nous aurons besoin d'un fichier contenant toutes les fonctions en rapport avec notre bras et notre interpréteur de commandes. Tiens, pourquoi ne pas l'appeler bras.c ? Ça colle bien, vous ne trouvez pas ? Et soyons fous, appelons bras.h le fichier interface du fichier bras.c. Il contiendra alors tous les prototypes nécessaires.
Ce n'est pas tout. Eh oui, ne l'oublions pas mais nous allons avoir besoin de manipuler une liste doublement chaînée. Il va donc falloir un fichier source liste.c contenant l'implémentation de toutes les fonctions nécessaires à la manipulation de notre liste et bien évidemment un fichier liste.h contenant la définition de notre liste et tous les prototypes correspondants.
Enfin, dans notre programme nous allons avoir besoin d'allouer une certaine quantité de mémoire. Nous allons donc créer un fichier memoire.c contenant des fonctions utilitaires de gestion de la mémoire prenant directement en compte la gestion des erreurs et un fichier memoire.h qui va avec.
Pour résumer, l'organisation de notre projet sera la suivante :

Maintenant que nous avons découpé notre projet en fichiers distincts, nous allons devoir faire un effort de réflexion supplémentaire afin de découper chaque fichier en fonctions distinctes (sous-programmes). Chaque fonction doit effectuer une tâche précise en rapport avec le fichier dans lequel elle est placée. En effet, nous n'allons pas placer notre fonction d'ajout d'un élément dans une liste chaînée dans le fichier bras.c, question de bon sens.
Voyons donc les principales fonctions dont nous allons avoir besoin. Tout d'abord, notre programme devra être constitué d'une boucle principale. Il s'agit là tout simplement de notre interpréteur de commandes. C'est l'élément central de notre programme, nous allons donc devoir créer une fonction de signature suivante : void bras(Liste *p_liste) où p_liste sera notre liste contenant les commandes saisies par l'utilisateur. Cependant, nous n'allons pas encapsuler tout notre code dans cette fonction. Nous allons alors créer plusieurs fonctions en rapport avec nos commandes, à savoir :

Signature

Rôle

void lire_commande(char commande[ LONGUEUR_MAX ]);

Lire une commande au clavier de façon sécurisée (utilisation de fgets).

int valide_commande(char commande[ LONGUEUR_MAX ]);

Vérifier si la commande saisie est valide ou non.

char const *derniere_commande(Liste *p_liste);

Retourner la dernière commande de la liste.

Enfin, afin de satisfaire la commande HELP nous aurons besoin d'une fonction void affiche_aide(void); ; nous aurons aussi besoin d'une fonction void visualisation(Liste *p_liste) permettant de satisfaire la commande VIEW. Nous venons donc de déterminer la liste des fonctions principales dont nous aurons besoin dans notre fichier bras.c.

Maintenant, voyons quelles fonctions nous seront utiles pour la manipulation de notre liste chaînée. Avant tout, nous aurons besoin d'allouer une nouvelle liste, une fonction Liste *nouvelle_liste(void); ne sera donc pas de refus. Ensuite, le sujet ne nous impose uniquement l'ajout de commandes, nous allons donc avoir besoin d'une fonction Liste *ajoute_requete(Liste *p_liste, char const *nom); . Enfin, une fois le programme terminé, il va nous falloir libérer toute la mémoire allouée. Ce sera le rôle d'une fonction void supprime_requete(Liste **p_liste); .

Il ne nous reste alors plus qu'à déterminer nos dernières fonctions de gestion de la mémoire. Nous allons tout d'abord mettre en place une fonction analogue à malloc, mis à part le fait que la gestion d'erreur sera intégrée à celle-ci. Lorsque malloc échouera, nous quitterons alors le programme. Enfin, nous allons avoir besoin d'une fonction de copie de chaîne de caractères dans une autre, utilisant notre fonction d'allocation précédemment évoquée. Notre fichier memoire.c sera donc constitué des deux fonctions que voici :

Signature

Rôle

void *xmalloc(size_t taille);

Allouer de la mémoire en prenant en compte les erreurs.

char *xstrdup(char const *s);

Dupliquer une chaîne dans une autre.

La plus grosse partie du travail a été réalisée. Désormais, il ne nous reste plus qu'à clarifier le dernier point de notre conception, à savoir la détermination des structures de données que nous allons devoir utiliser. En effet, dans un programme en langage C, il n'est pas rare d'avoir à utiliser des structures, énumérations, structures de données, etc. C'est donc ce que nous allons voir. Commençons alors par l'élément central : notre liste chaînée. Cette liste sera doublement chaînée. Nous allons créer un noeud nommé requete contenant le nom de la commande ainsi qu'un pointeur vers les éléments suivant et précédent. Ainsi, afin de faciliter les opérations sur notre liste, celle-ci sera constituée d'un pointeur vers le premier et le dernier noeud de la liste. Nous obtenons donc les structures suivantes :

typedef struct requete
{
    char *nom;

    struct requete *p_suivant;
    struct requete *p_precedent;
} Requete;

typedef struct liste
{
    struct requete *p_tete;
    struct requete *p_fin;
} Liste;

Ensuite, nous devons disposer d'une structure représentant la table des différentes commandes, ainsi que leur rôle et leur statut une fois effectuées. Cela nous donne donc la structure suivante :

struct table
{
    char const *nom;
    char const *role;
    char const *status;
};

que nous allons initialiser comme ceci :

static struct table commandes[] =
{ 
    { "UP", "Montee du bras", "Bras en position haute"              },
    /* reste des commandes */
};

Cette table étant uniquement utilisée dans notre fichier bras.c, nous la qualifions de static afin de réduire sa portée à ce fichier.

Enfin, afin d'éviter de renvoyer 0 ou 1 en cas de validité ou non de la commande saisie, nous allons créer une énumération :

enum { COMMANDE_INVALIDE, COMMANDE_VALIDE } ;

Nous renverrons alors soit COMMANDE_INVALIDE soit COMMANDE_VALIDE dans notre fonction valide_commande.


Le besoin Avant de commencer

Avant de commencer

Réfléchir avant d'agir Correction

C'est maintenant à vous de jouer. Vous avez toutes les cartes en main pour ne pas être bloqué lors de la réalisation du programme.
Cependant, avec que vous ne commenciez, il vous est nécessaire de prendre en compte ces quelques détails :

La temporisation

Une temporisation en langage C ne se réalise pas de la même façon selon le système d'exploitation que l'on utilise. Sous Windows, il existe la fonction Sleep déclarée dans le header <windows.h> permettant de mettre en pause le programme un certain nombre de millisecondes. Sous unixoïde, il existe la fonction usleep déclarée dans le header <unistd.h> . Ainsi, afin de n'utiliser qu'une fonction Sleep mettant en pause le programme un certain nombre de millisecondes, nous allons utiliser le bout de code suivant :

#if defined (WIN32) || defined (_WIN32)
    #include <windows.h>
#else
    #include <unistd.h>
    #define Sleep(x) usleep(x * 1000)
#endif

Si nous nous trouvons sous un autre système que Windows, nous définissons une macro Sleep identique à la fonction disponnible sous Windows.

La portée des fonctions

Afin d'aboutir à un programme propre, veillez à qualifier de static les fonctions que vous utiliserez uniquement dans un seul fichier. Par exemple, la fonction affiche_aide devra être static. Si vous le souhaitez, allez relire la partie du cours de M@teo21 sur le sujet se trouvant ici.

Protection des fichiers d'en-tête

Enfin, pensez aussi à protéger vos fichiers d'en-tête (.h) contre des éventuelles inclusions multiples. Si vous possédez Code::Blocks, celui-ci devrait vous proposer de le faire automatiquement. Si vous avez des doutes, n'hésitez pas à aller consulter le tutoriel de M@teo21 ici.

Voilà, c'est maintenant à vous de jouer, vous savez tout ce que vous aviez à savoir pour réaliser ce programme. Si vous êtes bloqués, ne regardez pas tout de suite la solution, et continuez à chercher. Si vraiment vous n'y arrivez pas, alors regardez la solution et essayez à tout prix de la comprendre. ;)


Réfléchir avant d'agir Correction

Correction

Avant de commencer Améliorations

Voici donc la correction, fichier par fichier, suivie des explications nécessaires à la compréhension du code.

Gestion de la mémoire

memoire.h

#ifndef MEMOIRE_H_INCLUDED
#define MEMOIRE_H_INCLUDED

#include <stddef.h>

void *xmalloc(size_t taille);
char *xstrdup(char const *s);

#endif

Pas de surprise, nous retrouvons notre fichier interface avec les prototypes de nos deux fonctions qui vont nous être utiles pour gérer la mémoire. Notez cependant la présence du header <stddef.h> , qui contient la déclaration du type size_t .

memoire.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "memoire.h"

void *xmalloc(size_t taille)
{
    void *p = malloc(taille);
    if (p == NULL)
    {
        perror("Erreur d'allocation memoire");
        exit(EXIT_FAILURE);
    }

    return p;
}

char *xstrdup(char const *s)
{
    char *p = xmalloc(strlen(s) + 1);
    return strcpy(p, s);
}

Comme vous le voyez, pour notre fonction xmalloc, nous commençons par demander à malloc de nous allouer la mémoire. Si celle-ci échoue (NULL renvoyé), nous affichons un message d'erreur sur la sortie standard grâce à la fonction perror. L'avantage de perror est qu'elle permet d'afficher la dernière erreur rencontrée dans le programme. Pour plus d'informations sur cette fonction, vous pouvez consulter le manuel.
Enfin, pour notre fonction xstrdup, nous réutilisons dans un premier temps notre fonction xmalloc afin d'allouer la taille nécessaire à la duplication de le chaîne passée en paramètre, en n'oubliant pas le +1 pour le '\0' final. strcpy renvoyant un pointeur sur notre chaîne de destination (à savoir p), nous utilisons directement son retour en guise de valeur renvoyée par notre fonction xstrdup. Nous utiliserons alors notre fonction comme ceci :

char *s = xstrdup("Bonjour tout le monde");
/* ... */
free(s);

Gestion de la liste chaînée

liste.h

#ifndef LISTE_H_INCLUDED
#define LISTE_H_INCLUDED

typedef struct requete
{
    char *nom;

    struct requete *p_suivant;
    struct requete *p_precedent;
} Requete;

typedef struct liste
{
    struct requete *p_tete;
    struct requete *p_fin;
} Liste;

Liste *nouvelle_liste(void);
Liste *ajoute_requete(Liste *p_liste, char const *nom);
void supprime_requetes(Liste **p_liste);

#endif

Ici, rien d'inattendu donc. Nous retrouvons bien notre liste chaînée préalablement déterminée dans notre étape de conception. Nous avons ensuite placé nos trois prototypes de fonctions qui vont nous servir à manipuler notre liste, à savoir la création d'une nouvelle liste (nouvelle_liste), l'ajout d'une commande dans la liste (ajoute_requete), et la suppression des requêtes une fois le travail fini (supprime_requetes).

liste.c

#include <stdlib.h>
#include "liste.h"
#include "memoire.h"

Liste *nouvelle_liste(void)
{
    Liste *nouveau = xmalloc(sizeof *nouveau);
    nouveau->p_fin = nouveau->p_tete = NULL;
    return nouveau;
}

Liste *ajoute_requete(Liste *p_liste, char const *nom)
{
    if (p_liste != NULL)
    {
        Requete *nouveau = xmalloc(sizeof *nouveau);
        nouveau->nom = xstrdup(nom);
        nouveau->p_suivant = NULL;
        if (p_liste->p_fin == NULL)
        {
            p_liste->p_fin = p_liste->p_tete = nouveau;
        }
        else
        {
            nouveau->p_precedent = p_liste->p_fin;
            p_liste->p_fin->p_suivant = nouveau;
            p_liste->p_fin = nouveau;
        }
    }
    return p_liste;
}

void supprime_requetes(Liste **p_liste)
{
    if (*p_liste != NULL)
    {
        Requete *parcours = (*p_liste)->p_tete;
        while (parcours != NULL)
        {
            Requete *supprime = parcours;
            parcours = parcours->p_suivant;
            free(supprime->nom);
            free(supprime);
        }
        (*p_liste)->p_tete = (*p_liste)->p_fin = NULL;
    }
}

Comme vous pouvez le constater, dans notre fonction nouvelle_liste, nous utilisons la fonction xmalloc que nous avons précédemment écrite. En cas d'erreur d'allocation mémoire, le programme sera directement quitté. Ensuite, cette ligne peut vous paraitre un peu inhabituelle :

nouveau->p_fin = nouveau->p_tete = NULL;

on retrouve ici deux opérateurs d'affectation '=' sur la même ligne. En fait, ceci est tout à fait valide et revient à écrire :

nouveau->p_fin = (nouveau->p_tete = NULL);

Cela permet alors d'initialiser différentes variables avec la même valeur (ici NULL), sur la même ligne.

Pour notre fonction ajoute_requetes, rien de bien particulier à redire, mis à part le fait qu'ici aussi nous utilisons notre fonction xmalloc ainsi que notre xstrdup afin de dupliquer (copier) le nom de notre commande dans notre champ nom. En fait, xstrdup fait tout simplement malloc + strcpy en une seule fonction. Si vous ne comprenez pas le reste de la fonction, relisez la théorie du tutoriel précédent.

De même, il n'y a rien de bien particulier à commenter au niveau de notre fonction supprime_requetes. Afin d'effectuer directement les modifications au niveau de la liste (suppression des éléments), nous devons en effet utiliser un pointeur double (Liste **). Nous n'oublions aussi pas de supprimer la mémoire allouée pour notre champ nom, sans quoi nous aurions des fuites de mémoire. Enfin, nous retrouvons la même syntaxe :

(*p_liste)->p_tete = (*p_liste)->p_fin = NULL;

pour complètement réinitialiser notre liste chaînée.

Le coeur du système

Enfin, nous allons maintenant nous attaquer au coeur du système, c'est-à-dire la gestion même de notre bras.

bras.h

#ifndef BRAS_H_INCLUDED
#define BRAS_H_INCLUDED

#include "liste.h"

void bras(Liste *p_liste);

#endif

Contrairement à ce qu'on pourrait attendre, nous ne retrouvons ici qu'un seul et unique prototype. Il s'agit de la fonction qui va se charger d'effectuer la boucle principale. Et pour cause, toutes les autres fonctions sont des fonctions utilitaires qui seront de portée privée (static), c'est-à-dire qui seront accessibles uniquement à notre fichier bras.c.

bras.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#if defined (WIN32) || defined (_WIN32)
    #include <windows.h>
#else
    #include <unistd.h>
    #define Sleep(x) usleep(x * 1000)
#endif

#include "liste.h"

#define LONGUEUR_MAX 25
#define TEMPO 500

struct table
{
    char const *nom;
    char const *role;
    char const *status;
} ;

static struct table commandes[] =
{
    { "UP", "Montee du bras", "Bras en position haute"              },
    { "DOWN", "Descente du bras", "Bras en position basse"          },
    { "ROTL", "Rotation gauche du bras", "Bras en butee gauche"     },
    { "ROTR", "Rotation droite du bras", "Bras en butee droite"     },
    { "FORWARD", "Avancee de la pince", "Pince avancee"             },
    { "BACKWARD", "Recul de la pince", "Pince reculee"              },
    { "OPEN", "Ouverture de la pince", "Pince ouverte"              },
    { "CLOSE", "Fermeture de la pince", "Pince fermee"              },
    { "VIEW", "Visulation des mouvements", NULL                     },
    { "REINIT", "Reinitialisation du bras en position defaut", NULL },
    { "HELP", "Affiche l'aide d'une commande", NULL                 },
    { "EXIT", "Quitte la console", NULL                             }
};

enum { COMMANDE_INVALIDE, COMMANDE_VALIDE } ;

static void affiche_aide(void)
{
    char const *aide = "UP : Montee du bras\n"
                       "DOWN : Descente du bras\n"
                       "ROTL : Rotation gauche du bras\n"
                       "ROTR : Rotation droite du bras\n"
                       "FORWARD : Avancee de la pince\n"
                       "BACKWARD : Recul de la pince\n"
                       "OPEN : Ouverture de la pince\n"
                       "CLOSE : Fermeture de la pince\n"
                       "VIEW : Visualisation des mouvements\n"
                       "REINIT : Reinitialisation du bras en "
                       "position defaut\n"
                       "HELP : Affiche l'aide d'une commande\n"
                       "EXIT : Quitte la console\n";
    puts(aide);
}

static struct table *infos(char commande[ LONGUEUR_MAX ])
{
    struct table *ret = NULL;
    size_t i, nombre_commandes = sizeof commandes / sizeof *commandes;
    for (i = 0; i < nombre_commandes; i++)
    {
        struct table *p = commandes + i;
        if (strcmp(p->nom, commande) == 0)
        {
            ret = p;
        }
    }
    return ret;
}

static void visualisation(Liste *p_liste)
{
    if (p_liste == NULL)
    {
        printf("Aucune requete parametree\n");
    }
    else
    {
        Requete *parcours = p_liste->p_tete;
        printf("Visulation en mode pas a pas\n");
        while (parcours != NULL)
        {
            struct table const *commande = infos(parcours->nom);
            printf("- %s\n", commande->role);
            Sleep(TEMPO);
            printf(". %s\n", commande->status);
            Sleep(TEMPO * 2);
            parcours = parcours->p_suivant;
        }
        printf("Fin de visualisation\n");
    }
}

static void fclean(char *buffer, FILE *fp)
{
    char *p = strchr(buffer, '\n');
    if (p != NULL)
    {
        *p = 0;
    }
    else
    {
        int c;
        while ((c = fgetc(fp)) != '\n' && c != EOF)
        {
        }
    }
}

static void lire_commande(char commande[ LONGUEUR_MAX ])
{
    printf("Bras:> ");
    fflush(stdout);
    fgets(commande, LONGUEUR_MAX, stdin);
    fclean(commande, stdin);
}

static int valide_commande(char commande[ LONGUEUR_MAX ])
{
    size_t i, nombre_commandes = sizeof commandes / sizeof *commandes;
    int ret = COMMANDE_INVALIDE;
    for (i = 0 ; i < nombre_commandes ; i++)
    {
        struct table const *p = commandes + i;
        if (strcmp(p->nom, commande) == 0)
        {
            ret = COMMANDE_VALIDE;
        }
    }
    return ret;
}

static char const *derniere_commande(Liste *p_liste)
{
    char const *ret = "";
    if (p_liste->p_fin != NULL)
    {
        ret = p_liste->p_fin->nom;
    }
    return ret;
}

void bras(Liste *p_liste)
{
    char commande[ LONGUEUR_MAX ] = "";
    do
    {
        lire_commande(commande);
        if (valide_commande(commande) == COMMANDE_INVALIDE)
        {
            printf("Erreur: commande '%s' inconnue\n", commande);
        }
        else if (strcmp(commande, "HELP") == 0)
        {
            affiche_aide();
        }
        else if (strcmp(commande, "REINIT") == 0)
        {
            supprime_requetes(&p_liste);
            printf("Bras en position par defaut\n");
        }
        else if (strcmp(commande, "VIEW") == 0)
        {
            visualisation(p_liste);
        }
        else
        {
            if (strcmp(derniere_commande(p_liste), commande) == 0)
            {
                printf("Attention: commande '%s' deja realisee\n", commande);
            }
            else
            {
                struct table const *p = infos(commande);
                p_liste = ajoute_requete(p_liste, commande);
                printf("Ajout commande %s (%s)\n", p->nom, p->role);
            }
        }
    }
    while (strcmp(commande, "EXIT") != 0) ;
    supprime_requetes(&p_liste);
}

Comme vous le voyez, ce fichier est le plus volumineux de notre projet, c'est lui qui contient le plus de lignes de codes. Normal, étant donné qu'il constitue le coeur de notre système. Pour des raisons pratiques, je ne pourrai expliquer de manière détaillée le fonctionnement intégral des différentes fonctions. C'est pourquoi je ne me contenterai d'expliquer que les grandes lignes. À commencer par notre fonction bras. Cette fonction est la fonction principale de notre fichier puisque c'est elle qui va gérer notre petit interpréteur de commandes. Tout d'abord, nous commençons par définir un tableau de caractères qui accueillera notre commande lue au clavier. Bien entendu, nous définissons aussi une taille maximum grâce à la constante LONGUEUR_MAX , ici définie à 25. Ensuite, nous rentrons dans notre boucle principale. Nous bouclons alors tant que nous ne rencontrons pas la commande EXIT (cela était bien spécifié par le cahier des charges). Afin de lire une commande au clavier, nous avons crée une fonction lire_commande, à laquelle nous passons notre tableau de caractères en paramètre. Comme vous pouvez le constater, et cela afin de sécuriser notre application, nous n'utilisons pas la fonction scanf mais la fonction fgets . Ce procédé de saisie est expliqué dans l'annexe du tutoriel de M@teo21 à cette adresse.
Une fois notre commande lue et le caractère ' ' enlevé de notre tableau, nous devons vérifier si celle-ci est valide grâce à une fonction valide_commande qui retourne soit COMMANDE_VALIDE soit COMMANDE_INVALIDE, d'où l'intérêt de notre énumération. Celle-ci va alors effectuer une rechercher dans notre table de commandes, afin de vérifier si elle est présente ou non.

Si notre commande n'existe pas dans notre tableau, nous affichons alors une erreur comme spécifié dans le cahier des charges. Sinon, cela signifie qu'elle est bel et bien valide, et nous utilisons donc la fonction strcmp afin d'effectuer notre comparaison. Plusieurs fonctions sont alors appelées selon la commande saisie. Par exemple, si l'utilisateur saisit la commande HELP, alors la fonction affiche_aide est appelée. Sinon, si l'utilisateur saisit la commande VIEW, la fonction visualisation est appelée, et ainsi de suite.
Si aucune de ces commandes n'a été saisie, cela veut dire que l'utilisateur a saisi une commande permettant de faire effectuer un mouvement au bras. Dans ce cas-là, nous vérifions d'abord grâce à la fonction derniere_commande si cette commande n'a pas été saisie juste avant, auquel cas nous affichons un avertissement (warning). Si ce n'est pas le cas, alors nous pouvons ajouter notre commande à notre liste chaînée et signaler à l'utilisateur que celle-ci a bien été prise en compte.

Voilà, nous avons bouclé notre travail. Il ne nous reste plus qu'à écrire une fonction main() afin de passer à l'étape de test :

#include <stdio.h>
#include <stdlib.h>
#include "liste.h"
#include "bras.h"

int main(void)
{
    Liste *reqs = nouvelle_liste();
    bras(reqs);
    free(reqs);
    return 0;
}

Désormais, nous pouvons compiler notre programme et le tester en profondeur afin de détecter d'éventuels bogues et les corriger.


Avant de commencer Améliorations

Améliorations

Correction

Notre programme est désormais compilable et exécutable, mais ne nous arrêtons pas en si bon chemin. Il s'agit là d'un comportement assez minimaliste. Voici quelques idées d'améliorations que nous pourrions nous amuser à ajouter :

Ces trois petites commandes constituent alors des idées d'améliorations. Bien sûr, on pourrait continuer à améliorer notre programme. La seule limite étant notre imagination. ;)

Ce tutoriel / TP touche désormais à sa fin. J'espère que le sujet qui vous a été présenté vous a permis de mieux comprendre le principe des listes chaînées et qu'il vous a permis de pratiquer le langage C comme vous l'avez appris dans le tutoriel de M@teo21.
Bien d'autres sujets sont tout aussi imaginables pour pratiquer ce genre de concept. On pourrait par exemple imaginer de programmer un système de gestion de distributeur de boissons (façade administrateur avec l'ajout de boissons et façade utilisateur avec le choix d'une boisson).


Correction