Année universitaire 2002-2003
espace

Licence d'informatique
Module 4 - partie "C / shell"



Travaux dirigés 9 :

  • 1. Compilation séparée.
  • 2. Utilisation de l'utilitaire make.
  • 3. Les pointeurs génériques.
  • 4. Concept de pile.
  • 5. Exercice 1 : module de gestion d'une pile.
  • 6. Exercice 2 : fichier makefile.

  • 1. Compilation séparée.

    Lors de l'élaboration d'un gros projet, il est nécessaire de découper le programme en plusieurs "modules". Il est alors intéressant de pouvoir compiler ces différents modules séparément. Chacun des "fichiers sources" constituant un projet sera compilé séparément, ce qui aura pour résultat de produire un "fichier objet". Par la suite, les différents fichiers objets, ainsi que le code compilé des fonctions standard du C utilisées, seront "liés" grâce à "l'éditeur de liens", afin de produire le "fichier exécutable". Parmi les avantages de la compilation séparée, on peut citer :

    Généralement, en langage C, un module est constitué :

    Le fichier d'en-tête d'un module constitue en fait "l'interface" de ce module. Un utilisateur pourra être amené à faire appel aux éléments contenus dans un fichier d'en-tête, et devra pour cela inclure ce fichier dans son fichier source, grâce à une directive d'inclusion de fichier (mot-clé #include).

    Exemple :

    Remarques :

    2. Utilisation de l'utilitaire make.

    L'utilitaire make permet de gérer la compilation séparée d'un projet, de telle sorte que seules les commandes de compilation ou d'édition de liens nécessaires soient exécutées, lors de la mise à jour d'un fichier exécutable. L'utilisation de make nécessite l'écriture d'un "fichier de description des dépendances" entres les différents fichiers constituant un projet.

    2.1. Syntaxe du fichier de description des dépendances.

    Chaque dépendance est décrite selon la syntaxe suivante :

        fichier_cible:liste_fichiers_requis
        <tabulation>commande_associée

    Le nom du fichier qui doit être mis à jour est fichier_cible, et liste_fichiers_requis est la liste des noms de fichiers (séparés par des espaces) dont dépend fichier_cible et qui sont susceptibles d'être modifiés par l'utilisateur. En d'autres termes, ces fichiers sont nécessaires à la création de fichier_cible et, s'ils sont modifiés, alors fichier_cible doit être mis à jour. Une mise à jour sera donc effectuée si la date de dernière modification d'au moins un des fichiers requis est postérieure à la date de dernière mise à jour de fichier_cible. Quant à commande_associée, il s'agit de la commande qui va permettre la mise à jour de fichier_cible.Le symbole <tabulation> représente un caractère de tabulation. La présence de ce caractère est indispensable. Enfin, les lignes commençant par un caractère #, dans un fichier de description des dépendances, sont des lignes de commentaires.

    2.2. Lancement de l'utilitaire  make.

    Si le fichier de description des dépendances s'appelle makefile ou Makefile, il suffit de taper :

    et seules les tâches nécessaires seront exécutées. Si ce fichier porte un autre nom, par exemple fichier_dépendances.mk (on utilise conventionnellement l'extension .mk dans ce cas), il faut taper :

    L'utilitaire make va tout d'abord vérifier la nécessité de mettre à jour le premier fichier cible apparaissant dans le fichier de description des dépendances. Si ce fichier est à jour, aucune autre vérification ne sera réalisée. C'est pourquoi il est nécessaire de placer le fichier qui constitue l'objectif final en tête du fichier de description des dépendances (en général, il s'agit du fichier contenant le programme exécutable).

    Exemple :

    Remarque :

    3. Les pointeurs génériques.

    Le type "pointeur générique", ou "pointeur universel", est défini par :

    L'intérêt de ce type est de pouvoir écrire des fonctions "génériques", c'est-à-dire par exemple des fonctions qui peuvent être appelées avec des paramètres effectifs de types inconnus a priori, car un paramètre formel de type void *, comme une variable de type void *, peut recevoir comme valeur n'importe quelle adresse. Nous avons déjà rencontré au moins deux cas de "généricité" :

    On peut mesurer, sur ces deux exemples, tout l'intérêt que représente la généricité. On ne s'intéresse ici qu'aux fonctions génériques, et pas aux macro-instructions.

    Le type void * est indispensable à l'écriture de fonctions génériques. Il existe deux manières de rendre une fonction générique : soit elle retourne une valeur de type void *, soit un de ses paramètres formels au moins est de type void *

    3.1. Fonctions retournant une valeur de type void *

    Pour illustrer ce cas, nous allons prendre l'exemple de la fonction malloc. La valeur qu'elle retourne, de type void *, peut être affectée à un un pointeur de n'importe quel type, comme le montre l'exemple suivant :

    La valeur retournée par la fonction malloc peut également être affectée à une variable de type "pointeur générique", comme le montre la séquence suivante :

    Cependant, cette dernière possibilité est fortement déconseillée, car elle est sujette à de nombreuses restrictions, comme le montre la séquence suivante :

    Une possibilité pour rendre cette dernière séquence compilable est de convertir les expressions p, (p+1) et (p+2) en pointeurs de caractères, c'est-à-dire :

    Cette séquence est compilable et produit l'affichage de : OK

    3.2. Fonctions dont un paramètre formel au moins est de type void *

    Pour illustrer ce cas, nous allons donner l'exemple d'une fonction permettant d'échanger les valeurs pointées par deux pointeurs de même type, et ce quel que soit le type commun de ces deux pointeurs. Une telle fonction peut être écrite de la façon suivante :

    void echange(void *p1,void *p2,size_t taille)
    {
    char aux;
    size_t i;
    for (i=0;i<taille;i++)
    {
    aux=((char *)p2)[i];
    ((char *)p2)[i]=((char *)p1)[i];
    ((char *)p1)[i]=aux;
    }
    }

    taille représente le nombre d'octets occupés par les deux valeurs pointées.

    Les programmes programme1.c et programme2.c donnent deux exemples d'appels de la fonction générique echange :


    programme1.c

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

    void echange(void *p1,void *p2,size_t taille)
    {
    char aux;
    size_t i;
    for (i=0;i<taille;i++)
    {
    aux=((char *)p2)[i];
    ((char *)p2)[i]=((char *)p1)[i];
    ((char *)p1)[i]=aux;
    }
    }

    int main(void)
    {
    int i1=7,i2=5;
    printf("Avant échange : %d %d\n",i1,i2);
    echange(&i1,&i2,sizeof(int));
    printf("Après échange : %d %d\n",i1,i2);
    exit(0);
    }


    programme2.c

    #include <stdio.h>
    #include <stdlib.h>
    #define T 8

    void echange(void *p1,void *p2,size_t taille)
    {
    char aux;
    size_t i;
    for (i=0;i<taille;i++)
    {
    aux=((char *)p2)[i];
    ((char *)p2)[i]=((char *)p1)[i];
    ((char *)p1)[i]=aux;
    }
    }

    int main(void)
    {
    char ch1[T]="OK",ch2[T]="Va bene";
    printf("Avant échange : %s %s\n",ch1,ch2);
    echange(ch1,ch2,T);
    printf("Après échange : %s %s\n",ch1,ch2);
    exit(0);
    }

    Dans programme1.c, p1 et p2 reçoivent des adresses d'entiers, alors que dans programme2.c, p1 et p2 reçoivent des adresses de caractères.

    Attention :

    4. Concept de pile.

    Le concept de pile associe une façon de stocker les données en mémoire et une façon de gérer leur accès. Généralement, on utilise une liste chaînée pour gérer une pile. Lorsqu'une nouvelle donnée doit être enregistrée, elle est placée au sommet de la pile (fonction empiler), et lorsqu'une donnée doit être retirée, on extrait celle qui est située au sommet de la pile (fonction depiler), comme l'indiquent les schémas suivants :

    Empiler / Dépiler

    5. Exercice 1.

    Le but de cet exercice est d'écrire un module permettant la manipulation d'une pile. On souhaite que ce module soit "générique", c'est-à-dire qu'il permette de manipuler n'importe quel type de pile. Ce module se composera de deux fichiers, comme cela a été expliqué dans le paragraphe 1 :

    5.1. Écriture du fichier pile.h

    1) Dans le fichier pile.h, on doit définir le "type générique" cellule :

    Le premier champ de cette structure est un pointeur générique (cf. paragraphe 3) de nom p_info, qui pointera sur un "élément", alors que son deuxième champ est un pointeur sur un élément de type cellule. Voici la représentation graphique d'une pile constituée selon ce principe et contenant deux éléments :

    Pile

    La seule contrainte, pour un utilisateur de ce module, sera d'effectuer, juste avant l'empilage d'un nouvel élément, l'allocation dynamique (grâce à la fonction malloc) d'un espace mémoire qui permettra de stocker cet élément.

    2) Par ailleurs, on doit faire référence, dans le fichier d'en-tête pile.h (à l'aide du mot-clé extern, et en faisant suivre chaque en-tête de fonction d'un point-virgule), aux fonctions qui seront définies dans pile.c, à l'exception de la fonction pile_vide, qui sera définie de manière confidentielle dans le fichier pile.c, à savoir :

    5.2. Écriture du fichier pile.c

    1) Comme la plupart des fonctions définies dans pile.c utilisent le type cellule défini dans pile.h, il faut faire apparaître dans pile.c la directive d'inclusion de fichier suivante :

    2) Comme on utilise la constante NULL et les fonctions malloc et free, on doit inclure le fichier stdlib.h

    3) Par ailleurs, comme certaines fonctions définies dans pile.c utilisent la fonction pile_vide, il faut faire apparaître dans pile.c la définition confidentielle de cette fonction, d'en-tête :

    Cette fonction retourne 1 si la pile pointée par p_pile est vide, et 0 sinon.

    4) Enfin, il faut écrire dans pile.c la définition des fonctions énumérées précédemment (l'ordre d'apparition de ces fonctions est indifférent).

    Lors de la séance 7 de travaux pratiques, il sera demandé d'écrire un fichier, de nom gestion.c, contenant un programme qui fera appel aux différents éléments déclarés dans le fichier pile.h.

    6. Exercice 2.

    Écrire un fichier de description des dépendances, de nom makefile, permettant la compilation du fichier gestion.c et la production d'un fichier exécutable, de nom gestion. Pour lancer la compilation optimisée de gestion.c, il suffira de taper dans la fenêtre système :


    Ces pages ont été réalisées par A. Crouzil, J.D. Durou et Ph. Joly.
    Pour tout commentaire, envoyer un mail à crouzil@irit.fr, à durou@irit.fr ou à Philippe.Joly@irit.fr.