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 valeur100
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, danstab.c
leur définitions, et modifier progressivement lemain()
danstest.c
pour les tester :read_score()
: prend en paramètre le tableau, la variablecount
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 :
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éssaireFILE* 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
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
:
version bonus
Quelles différences avec la version simple ? dans quelle partie de la mémoire sont stockés les octets de la chainechar* 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 */
nom
pour chacune des deux versions ? pourquoi le&
en plus devantnom
?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
- Adaptez le code précédant pour que les fichiers soient maintenant de la forme :
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.
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) :
-
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éeallocate_array()
et la libération grace à une fonctionfree_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 :
a
, il faut donc écrire :
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 :