Date de première publication : 2015/10/06
On va s'intéresser à la programmation d'un Hall of Fame : on mémorisera les meilleures scores sur des jeux avec l'identiifant avec lequel on a joué...
Les informations seront stockées dans des structures particulières quue nous allons utiliser de deux manières différentes :
- séance 1 : un tableau de structures (statique ou dynamique)
- séance 2 : une liste chaînée
La base du TP se trouve là :
git clone https://gitlab.com/kiux/C4.git
Une saisie sécurisée
Jusqu'à maintenant, on vous a laissé utiliser la fonction scanf()
pour faire de la saisie. Cette fonction est dangereuse (il n'y a aucun test de dépassement de mémoire tampon), il faut donc plutôt privilégier la fonction
fgets()
. Celle-ci a deux avantages :
- permettre de saisir des espaces
- mais surtout de contrôler la taille de la chaîne de caractères.
Elle a, en revanche un inconvénient,
la chaîne intègre le plus souvent le caractère de fin de saisie '\n'
, ce qui ne nous intéresse pas forcément même s'il n'est plus nécessaire de l'enlever de la mémoire tampon associée au fichier.
Voici un usage tout simple :
char saisie[255];
fgets(saisie, 255, stdin);
Si la chaîne de caractères saisie représente un nombre, il est possible de l'obtenir par conversion avec des fonctions comme atoi()
"ascii to integer", atof()
, strtol()
ou bien encore sscanf()
. Les fonctions sscanf()
ne sont absolument pas déconseillées
On vous conseille de vous faire une petite fonction équivalente du fgets()
mais qui enlève le '\n'
de fin de saisie s'il est présent.
Le fichier de test associé au TP montre un petit exemple d'utilisation de fgets()
TEST(fgets) {
char exemple [] = "scanf, c'est pas bien\n";
LOG(exemple);
char chaine1[25];
char chaine2[10];
FILE * file = fmemopen(exemple, sizeof(exemple)+1, "r");
// REQUIRE ( NULL != file);
fgets(chaine1, 25, file);
LOG(chaine1);
fclose(file);
REQUIRE( strlen(exemple) == strlen(chaine1) );
file = fmemopen(exemple, sizeof(exemple)+1, "r");
// REQUIRE ( NULL != file);
fgets(chaine2, 10, file);
LOG(chaine2);
REQUIRE( strlen(exemple) > strlen(chaine2) );
fgets(chaine2, 10, file);
LOG(chaine2);
fclose(file);
}
LOG()
est une macro de type fonction qui affiche le message donné en paramètre sur la sortie d'erreur standard
#define LOG(A) do { \
fprintf(stderr, "%s\n", A); \
} while(0)
Structures et tableaux
Première rencontre avec un structure
- Créer une structure
struct donnee
, renomméedonnee_t
(partypedef
) qui contient les éléments suivants : - un score entier
- un nom de jeu, une chaîne de 100 caractères
- un alias de joueur, une chaîne de 40 caractères
La suite du code se met dans une fonction :
- Créer une variable locale
essai
de typestruct donnee
. - Initialiser chacun des champs avec les valeurs que vous voulez
- Afficher les differents champs d'
essai
- Recommencer avec un type
donnee_t
Ce qu'il faut faire pour le TP...
- Récupérer la base de code fournie. Cela pourrait être également une bonne idée de placer le code sur votre propre dépôt git
- Ajouter la définition de la structure de donnée dans le fichier hall_of_fame.h
- Regarder quelle est la taille en mémoire de la structure créée, notamment par rapport à la taille des types simples comme
char
etint
TEST(Sizeof) {
int taille1 = sizeof(struct donnee);
int taille2 = sizeof(int)+100*sizeof(char); // :-)
CHECK (taille1 == taille2);
}
- Écrire la fonction
afficherDonnee()
qui prend en paramètre une structure et l'affiche sur le flux passé en paramètre.
TEST(AffichageB) {
// declaration et initialisation d'une variable de type struc
struct donnee essai;
strcpy(essai.nom, "pokemon GO");
strcpy(essai.alias, "loic");
essai.score = 498;
afficherDonnee(stdout, essai);
// creation du flux de texte => buffer
char buffer[1024];
FILE * file = fmemopen(buffer, 1024, "w");
REQUIRE ( NULL != file);
afficherDonnee(file, essai);
fclose(file);
CHECK( 0==strcmp(buffer, "pokemon GO : loic avec 498\n") );
}
Il y a deux tests différents (B et C) pour l'affichage. Cela permet de vérifier que les types struct donnee
et donnee_t
sont bien utilisables dans le programme.
Si le test est correct, vous pouvez sauvegarder ("commit") votre travail sur le dépôt
- Écrire une fonction
saisirDonnee()
qui permet de saisir les données de la structure en paramètre (sic !) à partir du flux donné. Regardez bien le test pour voir le protype et l'utilisation de la fonction tels que je les ai imaginés.
TEST(Saisie) {
struct donnee essai;
char buffer[1024];
strcpy(buffer, "rien\ndutout\n10");
FILE * file = fmemopen(buffer, 1024, "r");
// REQUIRE ( NULL != file);
saisirDonnee(file, &essai);
fclose(file);
afficherDonnee(stdout, essai);
CHECK( 0 == strcmp(essai.nom, "rien") );
CHECK( 0 == strcmp(essai.alias, "dutout") );
CHECK( 10 == essai.score );
}
Là encore, la fonction saisirDonnee()
n'ouvre pas le flux (fichier ou autre), elle ne fait que l'utiliser.
Le test unitaire proposé n'impose rien sur le type de retour de la fonction saisirDonnee()
. Il pourrait être pertinent que la fonction renvoie 0
si un problème a eu lieu pendant la saisie (fgets()
revoie NULL
en cas de problème) et une valeur non nulle sinon.
Pour cette question, il est recommandé d'avoir écrit la petite fonction qui enlève le saut de ligne '\n'
en dernier caractère !
- Définir une constante symbolique
TAILLE_MAX
d'au moins 50 dans le fichier .h adéquat - Écrire la fonction
tableauFromFilename()
qui permet de lire les données à partir d'un fichier texte dont voici un exemple avec deux jeux :
2048
loic
64236
Minecraft
kiux
12304883
Si le fichier contient plus de TAILLE_MAX
éléments, les éléments supplémentaires devront être écartés.
TEST(lectureFichier) {
donnee_t tableau[TAILLE_MAX];
int taille = 0;
// test d'un fichier non existant
taille = tableauFromFilename("inconnu.txt", tableau);
CHECK( 0 == taille );
// test du fichier exemple
taille = tableauFromFilename("jeu1.txt", tableau);
REQUIRE( 2 == taille );
CHECK ( 0 == strcmp(tableau[0].nom, "2048"));
CHECK ( 0 == strcmp(tableau[0].alias, "loic"));
CHECK ( 64236 == tableau[0].score );
CHECK ( 0 == strcmp(tableau[1].nom, "Minecraft"));
CHECK ( 0 == strcmp(tableau[1].alias, "kiux"));
CHECK ( 12304883 == tableau[1].score );
}
Aller plus loin
- Si vous avez utilisé un tableau statique, vous pouvez désormais utiliser un tableau dynamique
- Les chaînes de caractères de la structure de données pourraient également être allouées dynamiquement
- Écrire une fonction qui permet de sauvegarder le tableau dans un fichier texte
- Écrire un programme qui réutilise ces fonctions et qui permet à l'utilisateur de gérer une liste de scores
Listes chaînées
Nous allons mettre en œuvre la compilation séparée pour cet exercice. Vous aurez potentiellement deux exécutables :
- celui de tests issu de hall_of_tests.c
- le programme qui utilise ce qui a été écrit, aka main.c
On réutilise les fichiers hall_of_fame.h et hall_of_fame.c
Pour la suite de ce TP, on laisse tomber temporairement la bibliothèque de tests. Il faut d'abord manipuler les différents éléments avant de tester :-)
Il est également hyper important de ne pas perdre le travail précédent :
- si vous utilisez git, créez une nouvelle branche !
- dans le cas contraire, sauvegardez votre répertoire de travail avant d'apporter des modifications sur le code déjà écrit.
Création de la liste chaînée
Pour écrire une liste chaînée, vous devez choisir si vous la définissez avec une tête réelle ou une tête fictive.
- Documenter impérativement le choix retenu pour la tête de la liste dans le fichier d'entête
hall_of_fame.h
- Modifier la structure
struct donnee
dans le fichier d'entête en ajoutant un champ "suivant". La structure est désormais de type autoréférentielle. - Initialiser la liste chaînée dans le
main()
(fichiermain.c
). Dans le cas d'une liste à tête fictive, il peut être opportun d'écrire une fonctioncreerListe()
qui renvoie une liste vide.
struct donnee creerListe(void);
- Ajouter à la liste à la main un premier élément alloué dynamiquement et l’afficher.
- Insérer à la main en fin de liste un deuxième élément et afficher la liste
- Insérer à la main en tête un autre élément et afficher la liste
- Écrire maintenant une fonction
afficherListe()
qui affiche la liste. Remplacer les lignes de codes précédentes par l'appel à cette fonction (cela inclut la chaîne vide) et vérifier que cela marche toujours !
afficherListe(struct donnee tetefictive);
afficherListe(struct donnee * tetereelle);
- Écrire une fonction pour insérer un élément en tête de liste
insererListe()
- Écrire le code pour rendre la mémoire associée à la liste chaînée. Vérifier avec valgrind
- Écrire maintenant une fonction pour rendre la mémoire associée à la liste chaînée
libererListe()
. Cette fonction devra être testée pour une liste vide, une liste à un élément et une liste à un nombre quelconque d'éléments. Chaque test est validé par valgrind
valgrind est un véritable couteau suisse : il est impératif d'utiliser ce programme pour vérifier le rendu mémoire, mais vous devez aussi l'utiliser pour éliminer les erreurs de contexte. Si vous avez un segfault, c'est une bonne habitute de le lancer pour voir ce que le programme vous dit !
Utilisation de la liste chaînée.
Vous allez écrire un nouveau programme (main2.c
qui utilise les fonctions définies dans les questions précédentes et vous allez enrichir
les fonctionnalités disponibles
Le programme principal est constitué principalement d'un menu à base de switch
- Afficher la liste chaînée
- Insérer (en tête) un nouvel élément
- Quitter
- Éditer/modifier un élément
- Lire / sauvegarder à partir d'un fichier texte
L'exécution du programme sera vérifiée avec valgrind.
Aller plus loin
- Si vous avez utilisé une tête fictive, il faudrait recommencer avec une tête réelle
- Allouer dynamiquement les chaînes de caractères
- Si vous utilisez un makefile, la règle principale vise à générer le programme d'utilisation. Vous pouvez placer une règle spécifique, pour compiler le programme de tests.