Skip to content

Manipulation de tableaux, allocation dynamique et structures

Prérequis

avoir terminer le TP précédant (y compris la lecture des compléments de cours sur make et sur les structures)

On veut stocker dans un tableau les notes de plusieurs élèves et dans un autre tableau les coefficients. À partir de ces données, on souhaite écrire plusieurs fonctions permettant de calculer les moyennes.

Version simple

Pour cette première version, vous écrirez un fichier test.c contenant votre fonction main() ainsi que deux fichiers tab.h et tab.c. Le premier contiendra la déclaration des fonctions tandis que le second contiendra leurs définition.
Les deux fichiers .c inclueront le fichier .h. Vous écrirez un fichier makefile permettant de compiler avec l'utilitaire make séparemment les deux unités de compilation :

  • gcc -c -Wall -ansi -pedantic tab.c
  • gcc -c -Wall -ansi -pedantic test.c

et de générer un executable :

  • gcc tab.o test.o -o prog

Dans la fonction main() :

  • déclarer une constante MAX de valeur 100

Rappel

une constante en C n'est pas une variable, elle n'occupe pas de place en mémoire. C'est un marqueur que le compilateur rempalcera avant compilation. On l'implémente grace à #define NOM valeur (remarquez qu'il n'y a pas de ;)

  • déclarer un tableau de float de pile de taille MAX pour stocker les notes
  • déclarer un tableau de float de pile de taille MAX pour stocker les coefficients
  • déclarer une variable entière count initialisée à 0

  • Ajouter dans tab.h les déclarations des fonctions suivantes, dans tab.c leur définitions, et modifier progressivement le main() dans test.c pour les tester :

    • read_score() : prend en paramètre le tableau, la variable count par adresse (car il va falloir la modifier), le nom d'un étudiant, et le nom d'un fichier contenant les notes de tous les étudiants dans une matière, de la forme :

    nom1 note
    nom2 note
    ...
    
    La fonction doit trouver la bonne ligne et extraire la note de l'étudiant passé en paramètre pour la rajouter au tableau. La fonction renvoit 1 en cas de succès, 0 en cas d'erreur (par exemple si le fichier était mal formé, ou ne contenait pas l'étudiant...).

    Extraction d'une ligne d'un fichier

    Pour extraire une ligne d'un fichier, il faut procéder en trois étapes :

    • ouvrir le fichier en lecture pour obtenir un descripteur de fichier utilisable avec les fonctions standards du C
      FILE* desc = fopen("nom_fichier.txt","r"); 
      if(desc==NULL){ /* problème : impossible d'ouvrir ce fichier */
          fprintf(stderr, "Impossible d'ouvrir le fichier") /* écriture d'un avertissement sur la sortie d'erreur standard */
          /* ... quoi faire ? surement terminer le programme, ou au moins la fonction courante... */
      } 
      
    • utiliser la fonction getline une première fois pour obtenir la première ligne, puis recommencer autant que necéssaire
      FILE* desc; /* = ?? cf plus haut*/
      int nread;
      char * line;
      int len;
      while ((nread = getline(&line, &len, desc)) != -1) {
          printf("Retrieved line <%s> of length %d:\n",line, nread);
          /* remplacer l'affichage par le traitement voulu sur la ligne (tip : rappellez vous sscanf() (voir plus bas)*/         
      }
      free(line); /* A ne faire qu'après avoir terminé d'utiliser getline()*/
      
    • fermer le descripteur de fichier
      close(desc);
      

    Extraction des informations d'une ligne

    Afin de ne pas écrire du code trop complexe et trop long dans la fonction read_score() cela peut être une bonne idée d'écrire une fonction intermédiaire pour traiter une ligne. Cette fonction devra analyser la ligne pour savoir si elle concerne l'étudiant recherché, et extraire la note associée. Il faudra qu'elle renvoit la note et l'information "est-ce la ligne recherchée ?", donc une des deux info devra être retournée grace à une variable passée par adresse.

    • extraction des informations, grace à sscanf :
      #define MAX_NAME_LEN 15
      char nom[MAX_NAME_LEN];
      int note;
      char * line = ...
      sscanf(line," %s %d",nom,note);
      

    version bonus

    char* nom;
    int note;
    char * line = ...
    sscanf(line," %ms %d",&nom,note);
    /* utilisation de la variable nom */
    free(nom); /* a ne pas oublier quand on en a plus besoin */
    
    Quelles différences avec la version simple ? dans quelle partie de la mémoire sont stockés les octets de la chaine nom pour chacune des deux versions ? pourquoi le & en plus devant nom ?

    • compute_average() : prend le tableau issu de la longue étape précédente qui contient toutes les notes d'un élève, calcule la moyenne en considérant que les coefficients sont tous les mêmes.
    • print_student(char* nom, char* units[]) qui prend en paramètre le nom d'un étudiant et un tableau de nom de fichiers de notes et affiche la moyenne pour chaque matière de l'étudiant sur la sortie standard
  • adaptez votre programme pour qu'il prenne en argument le nom de l'étudiant puis la liste des fichiers de notes (pratique, c'est déjà le bon type pour le tableau à donner à print_student() !). Pour l'instant vous choisissez arbitrairement l'étudiants pour lequel on extrait les notes

Exemple

$> ./prog Pierre anglais.txt math.txt histoire.txt
Moyenne de Pierre 12
$>
  • Adaptez le code précédant pour que les fichiers soient maintenant de la forme :
coefficient
nom1 note
nom2 note
nom3 note
...

Le code doit manipuler 2 tableaux : le tableau de notes et un nouveau tableau de coefficients. La fonction read_score doit remplir les 2 tableaux. La fonction compute_average doit prendre en compte les coefficients dans le calcul de la moyenne.

Version 2 : tableau alloué dynamiquement

Le point faible de notre programme est qu'il utilise des tableaux de taille 100 en mémoire, alors que le nombre de matières est inconnu. Pour pouvoir utiliser un tableau de la bonne taille, il faut pouvoir l'allouer dynamiquement, c'est à dire que sa taille sera connue uniqument lors de l'exécution (et pas par le compilateur), et que les données seront stoquée sur le tas et non sur la pile.

Pour obtenir une zone de mémoire utilisable sur le tas, il faut utiliser la fonction malloc. Cette dernière prend en paramètre un entier : la taille en octets de la zone mémoire désirée.

La fonction malloc renvoie un void *, un type qui est compatible avec tous les autres types de pointeurs : c'est bien pratique, ca permet d'avoir une fonction unique pour allouer ce qu'on veut !

Warning

Si il n'y a pas assez de mémoire disponible, malloc renverra la valeur spéciale NULL. Il faut donc toujours vérifier que malloc ne nous à pas renvoyer NULL avant d'utiliser le pointeur retourné !

Si nous revenons à notre tableau de 100 float : quelle est la taille en octets de ce tableau ?

Example

100 fois la taille d'un float ! Mais quelle est la taille d'un float ? Vous n'avez pas à la savoir ! c'est le but de l'opérateur sizeof() dont l'utilisation ressemble à l'appel d'une fonction, sauf qu'on lui donne en paramètre un type.

int taille = sizeof(float);

Voici un code complet qui permet d'allouer un tableau de 23 entiers sur le tas :

int * tab = (int*) malloc(23*sizeof(int));
if(tab==NULL){
    fprintf(stderr, "Erreur d'allocation\n");
    exit(1);
}

Durée de l'allocation ?

Contrairement à une allocation sur la pile, qui est valable jusqu'à la fin de la fonction, l'allocation dynamique n'a pas de fin implicite. La mémoire reste réservée jusqu'à une libération explicite, grace à la fonction free(). Il ne faut pas oublier de libérer la mémoire quand on ne l'utilise plus, sous peine de dégrader les performences, et dans les cas extrèmes de provoquer une fin prématuée du programme si celui ci demande de la mémoire et qu'il n'y en a plus de disponible.

  • Soit le code suivant, expliquez pourquoi il ne peut pas être correct (2 raisons) :

    int* alloue_tableau(int taille)
    {
        int tab[taille];
        return tab;
    }
    

  • Que faut il faire alors ? (réponse : utiliser malloc())

  • Adaptez le programme de la première partie pour que les tableaux soient alloués dynamiquement avec comme taille la valeur argc-2 (pourquoi ?). Vous ferez l'allocation dans une fonction séparée allocate_array() et la libération grace à une fonction free_array()

Version 3 : avec une structure

  • Définissez une structure contenant les deux pointeurs de float (les tableaux) et la valeur count.
  • Faites une fonction d'allocation pour cette structure (sizeof() fonctionne aussi avec un type structuré)
  • modifiez votre programme pour utiliser cette structure plutot que les trois variables séparémment

Pointeurs dans une structure

Si une structure contient un champs a de type int * (par exemple), si on dispose d'une variable v du type de la structure, pour accéder à son champs, il faut écrire :

v.a;
Si on veut accéder à la valeure entière pointée par a, il faut donc écrire :
*(v.a);
Les parenthèses sont nécessaires, sinon on ne pourrait pas savoir si l'opérateur s'applique à v ou à v.a. Afin de lever cette ambiguité sans mettre de parenthèses, on peut également réécrire avec la syntaxe :
v->a;