Date de première publication : 2015/10/06
Pour ce TP, un petit exercice inspiré par La Poste, un rappel sur les fichiers textes vus pendant les semaines bloquées et le gros morceau : du calcul vectoriel.
Gestion de guichets
Vous devez gérer un bureau de poste qui dispose de 3 guichets : guichet_A
, guichet_B
, guichet_C
.
Vous n'avez pas le choix pour les noms et le type des variables, ça vient d'un antique programme à maintenir. Pour chaque guichet, une variable globale entière prend 0
si le guichet est fermé, 1
si celui est ouvert.
Écrire une fonction qui ferme tous les guichets et qui en ouvre un au hasard. Vérifier que cela marche bien !
Vous avez fait cela à la main ?
Maintenant, imaginons qu'il y ait beaucoup de guichets, 15 par exemple,de guichet_A
à guichet_O
. Voulez-vous le faire encore à la main ?
Comment peut-on en gardant les noms des guichets faire cela de manière automatique ?
Je mets une piste ci-dessous :
int * tab[NB];
Et le développement de cette piste :
int * tab[NB];
tab[0] = &guichet_A;
tab[1] = &guichet_B;
//...
//for(i;;) *tab[i] = 0
Rappel sur les fichiers
Vous avez manipulé les fichiers texte en C pendant les semaines bloquées, mais voici quelques rappels et compléments.
Tout d'abord, il est nécessaire d'ouvrir le fichier soit pour le lire ("r"
) soit pour l'écrire ("w"
) (il y a d'autres modes d'ouverture). Les données associées au fichier sont stockées dans une structure de type FILE
dont la fonction fopen()
renvoie l'adresse :
FILE * fichier;
fichier = fopen("nom_du_fichier", "r");
if (fichier) {
printf("on peut lire le fichier\n");
// utilisation
// fclose(fichier);
} else printf("on ne peut pas l'utiliser\n");
Si le fichier a pu être ouvert, il faut le fermer proprement avec la fonction fclose()
Pour manipuler le fichier, vous pourrez utiliser les fonctions suivantes :
float reel;
char chaine[80];
fprintf(fichier, "du texte %d", 10);
fscanf(fichier, "%f", &reel);
fgets(chaine, 80, fichier);
Pour savoir si tout s'est bien passé (écriture ou lecture), n'hésitez pas à consulter les valeurs de retour de ces fonctions. La fonction feof()
vous renseigne également sur la fin de fichier.
Produit scalaire de deux vecteurs
Nous allons manipuler des vecteurs mathématiques, des tableaux de nombres réels de taille variable.
Si vous avez compris ce qu'est l'allocation dynamique, vous allez manipuler des pointeurs. Dans le cas contraire, vous manipulerez d'abord un tableau de "grande" taille, puis, quand tout marchera, vous passerez à l'allocation dynamique.
Pour manipuler des vecteurs de nombres réels, vous avez le choix entre la simple précision float
ou double précision double
. Je vous propose de créer (ou plutôt de renommer) le type qui vous intéresse. Pour ce faire, il suffit d'ajouter la ligne suivante en début de programme :
typedef float * VecteurType;
// OU
typedef double * VecteurType;
typedef double VecteurType[255];
// OU
typedef float VecteurType[255];
Dans le cas de l'allocation statique, l'écriture est spéciale mais correspond bien à un vecteur de float
.
Avec typedef
, on peut toujours utiliser conjointement l'ancien et le nouveau nom.
Nous allons utiliser le développement "orienté tests" avec la bibliothèque introduite au premier TP.
Mise en place du code
J'ai créé un petit dépôt avec la base du code pour ce TP. Il est récupérable à l'adresse suivante :
git clone https://gitlab.com/kiux/C3.git
Plusieurs fichiers sont disponibles :
- vecteur.h qui contient les déclarations des fonctions que vous devez écrire dans le TP et les types définis. Ce fichier entête est inclus mais pas compilé.
- vecteur.c qui contient la définition (ou encore implémentation aka le code) des fonctions que vous devez écrire. Ce fichier doit être compilé et lié à l'exécutable final.
- tests_vecteur.c : un fichier qui comprend tous les tests. Les tests vont être décommentés au fur et à mesure. Ce fichier contient la fonction
main()
nécessaire au programme de tests. Ce fichier est compilé et lié. - teZZt.h et teZZt.c sont les fichiers d'une petite bibliothèque de tests. Elle est en développement et son fonctionnement n'est garanti qu'avec gcc. Là encore teZZt.h est inclus par les fichiers d'entête ou de code et teZZt.c est compilé et lié à l'exécutable de tests.
- des fichiers de données d'extension txt
Pour compiler le tout, il suffit de faire :
gcc *.c -o prog -Wall -Wextra -g
Si vous voulez, vous pouvez écrire le makefile correspondant.
Créations de vecteurs et premières manipulations
- Écrire une fonction d’affichage de vecteur. Le flux (
FILE *
), le vecteur lui-même et son ordre (sa taille) seront passés en paramètre.
Le prototype est caché ci-dessous :
void vecteurToFile(FILE * flux, float * vecteur, int ordre);
/* OU */
void vecteurToFile(FILE * flux, VecteurType vecteur, int ordre);
Pour écrire sur la sortie standard, il suffira de faire :
vecteurToFile(stdout, vecteur, ordre);
La fonction affiche l'ordre de vecteur sur une ligne puis composante par composante.
Pour les tests, on n'écrira pas directement dans un fichier (de type FILE *
) ou dans un flux standard - c'est un peu compliqué après pour comparer ce que l'on a écrit avec ce que l'on attendait. On écrira dans une chaine de caractères (nommée buffer
comme si cette chaîne était un fichier (la correspondance est faite avec la fonction fmemopen()
). Le test est alors bien plus facile à écrire ! (on ne compare pas deux fichiers externes ou un fichier externe et la sortie d'erreur redirigée dans un fichier, mais deux chaînes de caractères.
TEST(AffichageA) {
// vecteur statique a afficher
float v1 [] = { 1.0, 2.0, 3.0 };
// creation du flux de texte => buffer
char buffer[1024];
FILE * file = fmemopen(buffer, 1024, "w");
// REQUIRE ( NULL != file); // creation du fichier impossible ?
vecteurToFile(file, v1, 3);
fclose(file);
// verification de ce qui est envoye sur le flux
// chaque composante est affichee avec trois chiffres apres la virgule
// %.3f
CHECK( 0==strcmp(buffer, "3\n1.000 2.000 3.000") );
}
Si le test est un succès, rien n'est affiché. Si, en revanche, vous vous êtes trompés, vous le saurez !
Le fichier de test contient un autre test (AffichageB) avec un vecteur dynamique. Le test est arrêté si les allocations mémoire se passent mal.
Nous allons manipuler des vecteurs stockés dans des fichiers texte. Comme pour l'affichage, on trouvera tout d'abord l'ordre (la taille) du vecteur puis les composantes une à une :
3
1.0 2.0 3.0
- Écrire une fonction de lecture de fichier de vecteur. Le nom de fichier doit être passé en paramètre.
Si vous avez choisi d'allouer dynamiquement le vecteur, nous vous proposons de passer l'ordre du vecteur en paramètre (en lecture/écriture) et de renvoyer le vecteur. La valeur NULL
est renvoyée pour toute erreur d'allocation ou de lecture de fichier.
Pour une allocation statique, le vecteur sera passé en paramètre et la fonction retournera le nombre d'éléments du vecteur (potentiellement 0
).
Les prototypes sont cachés ci-dessous :
float * vecteurFromFileByName(const char * nom_fichier, int * p_ordre);
VecteurType vecteurFromFileByName(const char * nom_fichier, int * p_ordre);
int vecteurFromFileByName(const char * nom_fichier, float vecteur[]);
void vecteurFromFileByName(const char * nom_fichier, float vecteur[], int * p_ordre);
Faites attention à la position de la fin de fichier : s'agit-il de la fin de la ligne des composantes ou de celle
d'une ligne vide à la fin ? La fonction feof()
n'aura "pas" le même comportement. Si nécessaire, fscanf()
renvoie le nombre d'éléments effectivement lus.
Vous avez à votre disposition trois fichiers à savoir v1.txt
, v2.txt
et v3.txt
. On va d'abord vérifier les ordres des vecteurs contenus dans ces fichiers.
TEST(LectureA, "verification des ordres des vecteurs") {
int ordre;
vecteurFromFileByName("v1.txt", &ordre);
CHECK( 3 == ordre );
vecteurFromFileByName("v2.txt", &ordre);
CHECK( 6 == ordre );
vecteurFromFileByName("v3.txt", &ordre);
CHECK( 3 == ordre );
}
- Tester la fonction de lecture avec les autres fonctions. Vous pouvez créer vos propres tests si vous le voulez ou bien créer un autre exécutable pour mettre en situation votre code.
Le dernier test de lecture demande à ce que la fonction de lecture renvoie NULL
à la lecture de v3.txt
. Vous pouvez utiliser la fonction fgets()
pour lire une ligne entière puis faire un sscanf()
dessus. On utilisera intensivement la fonction fgets()
au prochain TP. Vous pouvez aussi considérer que l'ordre d'un vecteur est un nombre flottant puis le convertir en entier.
On va maintenant calculer le produit scalaire de deux vecteurs de même ordre X et Y. Pour rappel, on veut effectuer le produit suivant :
Les prototypes de la fonction sont cachés ci-dessous :
float produitScalaire(float * vecteur1, float * vecteur2, int ordre);
float produitScalaire(VecteurType vecteur1, VecteurType vecteur2, int ordre);
float produitScalaire(float vecteur1[], float vecteur2[], int ordre);
float produitScalaire(VecteurType vecteur1, VecteurType vecteur2, int ordre);
Je ne vous propose qu'un seul test pour le produit scalaire, le voici :
TEST(PVA) {
float * v1 = NULL;
float v2[3] = { 1.0, 2.0, 3.0};
int ordre = 0;
v1 = vecteurFromFileByName("v1.txt", &ordre);
CHECK ( 3 == ordre);
REQUIRE ( NULL != v1);
float ps = produitScalaire(v1, v2, ordre);
CHECK( EQ( 38, ps));
}
Vous avez déjà fait pas mal de boulot. Mais il est temps de regarder encore plus précisément ce que vous avez fait.
Pampers powers ...
Si vous avez fait l'exercice sans allocation dynamique, il est temps d'essayer avant de tenter de répondre aux questions de cette partie !
Vous avez fait des allocations dynamiques mais avez-vous pensé au rendu mémoire ? C'est facile de vérifier cela avec valgrind.. Compilez avec l'option -g et lancez votre nouvel outil préféré ....
Si vous ne fermez pas les fichiers avec un fclose()
, vous aurez une fuite mémoire sous valgrind.
- Ajouter une fonction
libererVecteur()
qui prend un vecteur en paramètre et qui libère la mémoire allouée. Pour que cela soit plus propre, on propose que le pointeur en paramètre soit réinitialisé à la valeur nulle. Le prototype est caché ci-dessous.
void libererVecteur(float * * p_vecteur);
void libererVecteur(VecteurType * p_vecteur);
Mettre le pointeur à NULL
n'est pas nécessaire si la libération se fait à la fin du programme mais c'est obligatoire si on
veut réutiliser un pointeur en cours de programme... La fonction free()
ne met pas à zéro le pointeur (sinon il faudrait lui passer l'adresse du pointeur dont on veut libérer la mémoire).
Dans le cas présent, si vous avez du mal avec les pointeurs, utiliser un type comme VecteurType
permet de
cacher un niveau de pointeur et de se ramener à quelque chose que l'on connaît.
- Il faut maintenant modifier le fichier de test ou le fichier de code de vecteur pour ne plus avoir de fuites mémoire !!!
Réutilisation de la compilation séparée et comparaison de réels
Vous allez maintenant créer un nouveau programme (main.c) qui va demander à l'utilisateur deux vecteurs et qui va déterminer si ces vecteurs sont orthogonaux (i.e. si leur produit scalaire est nul)
En informatique, on ne peut pas tester l'égalité de deux nombres réels avec l'opérateur =.
On teste la valeur absolue de la différence des nombres à comparer avec un nombre très petit (EPSILON
). L'option du compilateur
-Wfloat-equal
permet d'être averti de l'erreur
- l'égalité de deux réels peut être testée par une fonction qui renvoie vrai ou faux (1 ou 0), ou bien par une macro quand on saura ce que c'est !
- EPSILON est une constante symbolique à définir au bon endroit !
Produit matrice-vecteur
Si A est une matrice carrée d'ordre n et si X est un vecteur d'ordre n, on va calculer Y = A X
On va essayer de suivre la même démarche que pour la première partie. Il va donc falloir écrire les fonction suivantes :
matriceToFile()
pour l'affichagematriceFromFileName()
pour la lecture à partir d'un fichier
La structure suivante sera utilisée pour l'allocation dynamique de mémoire pour la matrice, qui se réalise en 2 étapes : nous allouons d'abord un tableau de n pointeurs (n étant le nombre de lignes de la matrice), puis nous faisons une allocation de n éléments (ici, n est le nombre de colonnes de la matrice) pour chaque pointeur. De cette manière, chaque pointeur du tableau pointe sur une ligne de matrice.
Voici un exemple de fichier, là encore il faut faire attention à l'endroit où est placée la fin de fichier :
3
1.0 2.0 3.0
2.0 4.0 5.0
3.0 7.0 1.0
Vous devez tester votre travail mais vous pouvez le faire comme vous voulez : en créant un nouveau fichier de tests ou bien avec un simple programme de démonstration.