tete du loic

 Loïc YON [KIUX]

  • Enseignant-chercheur
  • Référent Formation Continue
  • Responsable des contrats pros ingénieur
  • Référent entrepreneuriat
  • Responsable de la filière F2 ingénieur
  • Secouriste Sauveteur du Travail
mail
loic.yon@isima.fr
phone
(+33 / 0) 4 73 40 50 42
location_on
Institut d'informatique ISIMA
  • twitter
  • linkedin
  • viadeo

[C] TP 3 : Calculs

 Cette page commence à dater. Son contenu n'est peut-être plus à jour. Contactez-moi si c'est le cas!

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 :

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

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

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 );
}

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.

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.

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

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 :

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
  • Écrire une fonction de multiplication de matrice par un vecteur qui réutilise la fonction de produit scalaire écrite.
  • 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.